Zettelstore

Check-in Differences
Login

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

Difference From version-0.0.13 To trunk

2024-03-18
11:06
Fix regression of last commit (forgot to update tests) ... (Leaf check-in: 726c56c5ed user: stern tags: trunk)
11:01
Add computed zettel "Zettelstore Memory" ... (check-in: 20c2401591 user: stern tags: trunk)
2021-06-07
09:11
Increase version to 0.0.14-dev to begin next development cycle ... (check-in: 7dd6f4dd5c user: stern tags: trunk)
2021-06-01
12:35
Version 0.0.13 ... (check-in: 11d9b6da63 user: stern tags: trunk, release, version-0.0.13)
10:14
Log output while starting Command Line Server ... (check-in: 968a91bbaa user: stern tags: trunk)

Deleted .deepsource.toml.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
version = 1

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

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

Changes to LICENSE.txt.

1

2
3
4
5
6
7
8

1
2
3
4
5
6
7
8
-
+







Copyright (c) 2020-2021 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

1

2
3

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

.PHONY:  check 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/check/check.go -r

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

8
9
10
11
12
13
14






15
16
17
18
19
20

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

26







+
+
+
+
+
+





-
+
that are related to each other. Since knowledge is typically build up
gradually, one major focus is a long-term store of these notes, hence the name
“Zettelstore”.

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

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

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

[Stay tuned](https://twitter.com/zettelstore)…
[Stay tuned](https://mastodon.social/tags/Zettelstore) …

Changes to VERSION.

1


1
-
+
0.0.13
0.18.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
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

-
+

-
+




+
+
+


-
+





-
-
-
+
+
+





-

-
+



+




-
+








-
-
-







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

// Package ast provides the abstract syntax tree.
// 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 {
	// Zettel  domain.Zettel
	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.
type Node interface {
	Accept(v Visitor)
	WalkChildren(v Visitor)
}

// BlockNode is the interface that all block nodes must implement.
type BlockNode interface {
	Node
	blockNode()
}

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

// ItemNode is a node that can occur as a list item.
type ItemNode interface {
	BlockNode
	itemNode()
}

// ItemSlice is a slice of ItemNodes.
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
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







-
-
-















-
+



+



// InlineNode is the interface that all inline nodes must implement.
type InlineNode interface {
	Node
	inlineNode()
}

// InlineSlice is a slice of InlineNodes.
type InlineSlice []InlineNode

// Reference is a reference to external or internal material.
type Reference struct {
	URL   *url.URL
	Value string
	State RefState
}

// RefState indicates the state of the reference.
type RefState int

// Constants for RefState
const (
	RefStateInvalid  RefState = iota // Invalid Reference
	RefStateZettel                   // Reference to an internal zettel
	RefStateSelf                     // Reference to same zettel with a fragment
	RefStateFound                    // Reference to an existing internal zettel
	RefStateFound                    // Reference to an existing internal zettel, URL is ajusted
	RefStateBroken                   // Reference to a non-existing internal zettel
	RefStateHosted                   // Reference to local hosted non-Zettel, without URL change
	RefStateBased                    // Reference to local non-Zettel, to be prefixed
	RefStateQuery                    // Reference to a zettel query
	RefStateExternal                 // Reference to external material
)

Deleted ast/attr.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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







































































































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

// Package ast provides the abstract syntax tree.
package ast

import (
	"strings"
)

// Attributes store additional information about some node types.
type Attributes struct {
	Attrs map[string]string
}

// HasDefault returns true, if the default attribute "-" has been set.
func (a *Attributes) HasDefault() bool {
	if a != nil {
		_, ok := a.Attrs["-"]
		return ok
	}
	return false
}

// RemoveDefault removes the default attribute
func (a *Attributes) RemoveDefault() {
	a.Remove("-")
}

// Get returns the attribute value of the given key and a succes value.
func (a *Attributes) Get(key string) (string, bool) {
	if a != nil {
		value, ok := a.Attrs[key]
		return value, ok
	}
	return "", false
}

// Clone returns a duplicate of the attribute.
func (a *Attributes) Clone() *Attributes {
	if a == nil {
		return nil
	}
	attrs := make(map[string]string, len(a.Attrs))
	for k, v := range a.Attrs {
		attrs[k] = v
	}
	return &Attributes{attrs}
}

// Set changes the attribute that a given key has now a given value.
func (a *Attributes) Set(key, value string) *Attributes {
	if a == nil {
		return &Attributes{map[string]string{key: value}}
	}
	if a.Attrs == nil {
		a.Attrs = make(map[string]string)
	}
	a.Attrs[key] = value
	return a
}

// Remove the key from the attributes.
func (a *Attributes) Remove(key string) {
	if a != nil {
		delete(a.Attrs, key)
	}
}

// AddClass adds a value to the class attribute.
func (a *Attributes) AddClass(class string) *Attributes {
	if a == nil {
		return &Attributes{map[string]string{"class": class}}
	}
	classes := a.GetClasses()
	for _, cls := range classes {
		if cls == class {
			return a
		}
	}
	classes = append(classes, class)
	a.Attrs["class"] = strings.Join(classes, " ")
	return a
}

// GetClasses returns the class values as a string slice
func (a *Attributes) GetClasses() []string {
	if a == nil {
		return nil
	}
	classes, ok := a.Attrs["class"]
	if !ok {
		return nil
	}
	return strings.Fields(classes)
}

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
















































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

// Package ast provides the abstract syntax tree.
package ast_test

import (
	"testing"

	"zettelstore.de/z/ast"
)

func TestHasDefault(t *testing.T) {
	attr := &ast.Attributes{}
	if attr.HasDefault() {
		t.Error("Should not have default attr")
	}
	attr = &ast.Attributes{Attrs: map[string]string{"-": "value"}}
	if !attr.HasDefault() {
		t.Error("Should have default attr")
	}
}

func TestAttrClone(t *testing.T) {
	orig := &ast.Attributes{}
	clone := orig.Clone()
	if len(clone.Attrs) > 0 {
		t.Error("Attrs must be empty")
	}

	orig = &ast.Attributes{Attrs: map[string]string{"": "0", "-": "1", "a": "b"}}
	clone = orig.Clone()
	m := clone.Attrs
	if m[""] != "0" || m["-"] != "1" || m["a"] != "b" || len(m) != len(orig.Attrs) {
		t.Error("Wrong cloned map")
	}
	m["a"] = "c"
	if orig.Attrs["a"] != "b" {
		t.Error("Aliased map")
	}
}

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
21
22
23
24



25


26
27



28
29
30
31

32
33
34
35



36
37
38
39


40
41
42
43
44




45
46

47
48
49
50


51
52
53


54
55
56
57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56



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

-
+

-
+




+
+
+


-


+
+


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






-
-
-
+
+
+

+
+
-
-
+
+
+



-
+

-
-
-
+
+
+


-
-
+
+



-
-
+
+
+
+


+


-
-
+
+

-
-
+
+





-
-
+
+

-
+


-
-
+
+



-
+





-
-
+
+

-
-
+
+
+
+
+





-
-
-
+
+
+
+
-
+


-
-
+
+

-
-
+
+





-
+


-
-
+
+

-
-
+
+





-
+

-
+


-
-
+
+



-
+





-
-
+
+

-
-
+
+
+
+
+
+
+
+














-
+

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







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

// Package ast provides the abstract syntax tree.
package ast

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

// Definition of Block nodes.

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

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

// WalkChildren walks down to the descriptions.
func (bs *BlockSlice) WalkChildren(v Visitor) {
	if bs != nil {
		for _, bn := range *bs {
			Walk(v, bn)
		}
	}
}

// FirstParagraphInlines returns the inline list of the first paragraph that
// contains a inline list.
func (bs BlockSlice) FirstParagraphInlines() InlineSlice {
	for _, bn := range bs {
		pn, ok := bn.(*ParaNode)
		if !ok {
			continue
		}
		if inl := pn.Inlines; len(inl) > 0 {
			return inl
		}
	}
	return nil
}

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

// ParaNode contains just a sequence of inline elements.
// Another name is "paragraph".
type ParaNode struct {
	Inlines InlineSlice
}

func (pn *ParaNode) blockNode()       {}
func (pn *ParaNode) itemNode()        {}
func (pn *ParaNode) descriptionNode() {}
func (*ParaNode) blockNode()       { /* Just a marker */ }
func (*ParaNode) itemNode()        { /* Just a marker */ }
func (*ParaNode) descriptionNode() { /* Just a marker */ }

// CreateParaNode creates a parameter block from inline nodes.
func CreateParaNode(nodes ...InlineNode) *ParaNode { return &ParaNode{Inlines: nodes} }
// Accept a visitor and visit the node.
func (pn *ParaNode) Accept(v Visitor) { v.VisitPara(pn) }

// WalkChildren walks down the inline elements.
func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) }

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

// VerbatimNode contains lines of uninterpreted text
// VerbatimNode contains uninterpreted text
type VerbatimNode struct {
	Code  VerbatimCode
	Attrs *Attributes
	Lines []string
	Kind    VerbatimKind
	Attrs   attrs.Attributes
	Content []byte
}

// VerbatimCode specifies the format that is applied to code inline nodes.
type VerbatimCode int
// VerbatimKind specifies the format that is applied to code inline nodes.
type VerbatimKind int

// Constants for VerbatimCode
const (
	_               VerbatimCode = iota
	VerbatimProg                 // Program code.
	_               VerbatimKind = iota
	VerbatimZettel               // Zettel content
	VerbatimProg                 // Program code
	VerbatimEval                 // Code to be externally interpreted. Syntax is stored in default attribute.
	VerbatimComment              // Block comment
	VerbatimHTML                 // Block HTML, e.g. for Markdown
	VerbatimMath                 // Block math mode
)

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

// Accept a visitor an visit the node.
func (vn *VerbatimNode) Accept(v Visitor) { v.VisitVerbatim(vn) }
// WalkChildren does nothing.
func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ }

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

// RegionNode encapsulates a region of block nodes.
type RegionNode struct {
	Code    RegionCode
	Attrs   *Attributes
	Kind    RegionKind
	Attrs   attrs.Attributes
	Blocks  BlockSlice
	Inlines InlineSlice // Additional text at the end of the region
	Inlines InlineSlice // Optional text at the end of the region
}

// RegionCode specifies the actual region type.
type RegionCode int
// RegionKind specifies the actual region type.
type RegionKind int

// Values for RegionCode
const (
	_           RegionCode = iota
	_           RegionKind = iota
	RegionSpan             // Just a span of blocks
	RegionQuote            // A longer quotation
	RegionVerse            // Line breaks matter
)

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

// Accept a visitor and visit the node.
func (rn *RegionNode) Accept(v Visitor) { v.VisitRegion(rn) }
// WalkChildren walks down the blocks and the text.
func (rn *RegionNode) WalkChildren(v Visitor) {
	Walk(v, &rn.Blocks)
	Walk(v, &rn.Inlines)
}

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

// HeadingNode stores the heading text and level.
type HeadingNode struct {
	Level   int
	Inlines InlineSlice // Heading text, possibly formatted
	Slug    string      // Heading text, suitable to be used as an URL fragment
	Level    int
	Attrs    attrs.Attributes
	Slug     string      // Heading text, normalized
	Fragment string      // Heading text, suitable to be used as an unique URL fragment
	Attrs   *Attributes
	Inlines  InlineSlice // Heading text, possibly formatted
}

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

// Accept a visitor and visit the node.
func (hn *HeadingNode) Accept(v Visitor) { v.VisitHeading(hn) }
// WalkChildren walks the heading text.
func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) }

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

// HRuleNode specifies a horizontal rule.
type HRuleNode struct {
	Attrs *Attributes
	Attrs attrs.Attributes
}

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

// Accept a visitor and visit the node.
func (hn *HRuleNode) Accept(v Visitor) { v.VisitHRule(hn) }
// WalkChildren does nothing.
func (*HRuleNode) WalkChildren(Visitor) { /* No children*/ }

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

// NestedListNode specifies a nestable list, either ordered or unordered.
type NestedListNode struct {
	Code  NestedListCode
	Kind  NestedListKind
	Items []ItemSlice
	Attrs *Attributes
	Attrs attrs.Attributes
}

// NestedListCode specifies the actual list type.
type NestedListCode int
// NestedListKind specifies the actual list type.
type NestedListKind uint8

// Values for ListCode
const (
	_                   NestedListCode = iota
	_                   NestedListKind = iota
	NestedListOrdered                  // Ordered list.
	NestedListUnordered                // Unordered list.
	NestedListQuote                    // Quote list.
)

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

// Accept a visitor and visit the node.
func (ln *NestedListNode) Accept(v Visitor) { v.VisitNestedList(ln) }
// WalkChildren walks down the items.
func (ln *NestedListNode) WalkChildren(v Visitor) {
	if items := ln.Items; items != nil {
		for _, item := range items {
			WalkItemSlice(v, item)
		}
	}
}

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

// DescriptionListNode specifies a description list.
type DescriptionListNode struct {
	Descriptions []Description
}

// Description is one element of a description list.
type Description struct {
	Term         InlineSlice
	Descriptions []DescriptionSlice
}

func (dn *DescriptionListNode) blockNode() {}
func (*DescriptionListNode) blockNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (dn *DescriptionListNode) Accept(v Visitor) { v.VisitDescriptionList(dn) }
// WalkChildren walks down to the descriptions.
func (dn *DescriptionListNode) WalkChildren(v Visitor) {
	if descrs := dn.Descriptions; descrs != nil {
		for i, desc := range descrs {
			if len(desc.Term) > 0 {
				Walk(v, &descrs[i].Term) // Otherwise, changes in desc.Term will not go back into AST
			}
			if dss := desc.Descriptions; dss != nil {
				for _, dns := range dss {
					WalkDescriptionSlice(v, dns)
				}
			}
		}
	}
}

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

// TableNode specifies a full table
type TableNode struct {
	Header TableRow    // The header row
	Align  []Alignment // Default column alignment
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


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







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






-
-
-
+
+
+


-
+

-
-
+
+
	_            Alignment = iota
	AlignDefault           // Default alignment, inherited
	AlignLeft              // Left alignment
	AlignCenter            // Center the content
	AlignRight             // Right alignment
)

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

// WalkChildren walks down to the cells.
func (tn *TableNode) blockNode() {}

// Accept a visitor and visit the node.
func (tn *TableNode) Accept(v Visitor) { v.VisitTable(tn) }
func (tn *TableNode) WalkChildren(v Visitor) {
	if header := tn.Header; header != nil {
		for i := range header {
			Walk(v, &header[i].Inlines) // Otherwise changes will not go back
		}
	}
	if rows := tn.Rows; rows != nil {
		for _, row := range rows {
			for i := range row {
				Walk(v, &row[i].Inlines) // Otherwise changes will not go back
			}
		}
	}
}

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

// TranscludeNode specifies block content from other zettel to embedded in
// current zettel
type TranscludeNode struct {
	Attrs attrs.Attributes
	Ref   *Reference
}

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

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

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

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

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

// Accept a visitor and visit the node.
func (bn *BLOBNode) Accept(v Visitor) { v.VisitBLOB(bn) }
// WalkChildren does nothing.
func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ }

Changes to ast/inline.go.

1
2

3
4

5
6
7
8



9
10
11
12
13






14
15



























16
17
18
19
20
21

22
23
24
25

26
27

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

46


47
48





49
50
51
52
53
54
55
56
57

58
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


1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

56
57



58


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

-
+

-
+




+
+
+


-


+
+
+
+
+
+


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





-
+

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








-
+

+
+
-
-
+
+
+
+
+








-
+

-
-
+
+





+


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

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



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


-
+

-
-
+
+





+
-
-
+
+
-


-
+

-
-
+
+







+
+
-
+
+


-
+

-
-
+
+
+
+
+
+





-
-
+
+


-
+

-
-
+
+





-
-
+
+



-
-
+
+



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


-
+

-
-
+
+





-
-
-
+
+
+


-
-
+
+



-
-
-
-
+
+
+
+
+


+


-
+

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

// Package ast provides the abstract syntax tree.
package ast

import (
	"unicode/utf8"

	"zettelstore.de/client.fossil/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 (tn *TextNode) inlineNode() {}
func (*TextNode) inlineNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (tn *TextNode) Accept(v Visitor) { v.VisitText(tn) }

// WalkChildren does nothing.
// --------------------------------------------------------------------------

func (*TextNode) WalkChildren(Visitor) { /* No children*/ }
// TagNode contains a tag.
type TagNode struct {
	Tag string // The text itself.
}

func (tn *TagNode) inlineNode() {}

// Accept a visitor and visit the node.
func (tn *TagNode) Accept(v Visitor) { v.VisitTag(tn) }

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

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

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

// WalkChildren does nothing.
func (*SpaceNode) WalkChildren(Visitor) { /* No children*/ }
// Accept a visitor and visit the node.
func (sn *SpaceNode) Accept(v Visitor) { v.VisitSpace(sn) }

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

func (bn *BreakNode) inlineNode() {}
func (*BreakNode) inlineNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (bn *BreakNode) Accept(v Visitor) { v.VisitBreak(bn) }
// WalkChildren does nothing.
func (*BreakNode) WalkChildren(Visitor) { /* No children*/ }

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

// LinkNode contains the specified link.
type LinkNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Ref     *Reference
	Inlines InlineSlice // The text associated with the link.
}
	OnlyRef bool        // True if no text was specified.
	Attrs   *Attributes // Optional attributes
}

func (ln *LinkNode) inlineNode() {}

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

// WalkChildren walks to the link text.
func (ln *LinkNode) WalkChildren(v Visitor) {
	if len(ln.Inlines) > 0 {
		Walk(v, &ln.Inlines)
	}
}

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

// EmbedRefNode contains the specified embedded reference material.
type EmbedRefNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Ref     *Reference       // The reference to be embedded.
	Syntax  string           // Syntax of referenced material, if known
	Inlines InlineSlice      // Optional text associated with the image.
}
// Accept a visitor and visit the node.
func (ln *LinkNode) Accept(v Visitor) { v.VisitLink(ln) }

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

// WalkChildren walks to the text that describes the embedded material.
func (en *EmbedRefNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) }

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

// ImageNode contains the specified image reference.
type ImageNode struct {
	Ref     *Reference  // Reference to image
// EmbedBLOBNode contains the specified embedded BLOB material.
type EmbedBLOBNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Blob    []byte      // BLOB data of the image, as an alternative to Ref.
	Syntax  string      // Syntax of Blob
	Inlines InlineSlice // The text associated with the image.
	Syntax  string           // Syntax of Blob
	Blob    []byte           // BLOB data itself.
	Inlines InlineSlice      // Optional text associated with the image.
	Attrs   *Attributes // Optional attributes
}

func (in *ImageNode) inlineNode() {}
func (*EmbedBLOBNode) inlineNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (in *ImageNode) Accept(v Visitor) { v.VisitImage(in) }
// WalkChildren walks to the text that describes the embedded material.
func (en *EmbedBLOBNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) }

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

// CiteNode contains the specified citation.
type CiteNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Key     string      // The citation key
	Inlines InlineSlice // The text associated with the citation.
	Key     string           // The citation key
	Inlines InlineSlice      // Optional text associated with the citation.
	Attrs   *Attributes // Optional attributes
}

func (cn *CiteNode) inlineNode() {}
func (*CiteNode) inlineNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (cn *CiteNode) Accept(v Visitor) { v.VisitCite(cn) }
// WalkChildren walks to the cite text.
func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) }

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

// MarkNode contains the specified merked position.
// It is a BlockNode too, because although it is typically parsed during inline
// mode, it is moved into block mode afterwards.
type MarkNode struct {
	Mark     string      // The mark text itself
	Slug     string      // Slugified form of Mark
	Text string
	Fragment string      // Unique form of Slug
	Inlines  InlineSlice // Marked inline content
}

func (mn *MarkNode) inlineNode() {}
func (*MarkNode) inlineNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (mn *MarkNode) Accept(v Visitor) { v.VisitMark(mn) }
// WalkChildren does nothing.
func (mn *MarkNode) WalkChildren(v Visitor) {
	if len(mn.Inlines) > 0 {
		Walk(v, &mn.Inlines)
	}
}

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

// FootnoteNode contains the specified footnote.
type FootnoteNode struct {
	Inlines InlineSlice // The footnote text.
	Attrs   *Attributes // Optional attributes
	Attrs   attrs.Attributes // Optional attributes
	Inlines InlineSlice      // The footnote text.
}

func (fn *FootnoteNode) inlineNode() {}
func (*FootnoteNode) inlineNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (fn *FootnoteNode) Accept(v Visitor) { v.VisitFootnote(fn) }
// WalkChildren walks to the footnote text.
func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }

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

// FormatNode specifies some inline formatting.
type FormatNode struct {
	Code    FormatCode
	Attrs   *Attributes // Optional attributes.
	Kind    FormatKind
	Attrs   attrs.Attributes // Optional attributes.
	Inlines InlineSlice
}

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

// Constants for FormatCode
const (
	_               FormatCode = iota
	_            FormatKind = iota
	FormatItalic               // Italic text.
	FormatEmph                 // Semantically emphasized text.
	FormatEmph              // Emphasized text
	FormatBold                 // Bold text.
	FormatStrong               // Semantically strongly emphasized text.
	FormatStrong            // Strongly emphasized text
	FormatUnder                // Underlined text.
	FormatInsert               // Inserted text.
	FormatInsert            // Inserted text
	FormatStrike               // Text that is no longer relevant or no longer accurate.
	FormatDelete               // Deleted text.
	FormatSuper                // Superscripted text.
	FormatSub                  // SubscriptedText.
	FormatQuote                // Quoted text.
	FormatDelete            // Deleted text
	FormatSuper             // Superscripted text
	FormatSub               // SubscriptedText
	FormatQuote             // Quoted text
	FormatQuotation            // Quotation text.
	FormatSmall                // Smaller text.
	FormatSpan                 // Generic inline container.
	FormatMark              // Marked text
	FormatSpan              // Generic inline container
	FormatMonospace            // Monospaced text.
)

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

// Accept a visitor and visit the node.
func (fn *FormatNode) Accept(v Visitor) { v.VisitFormat(fn) }
// WalkChildren walks to the formatted text.
func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }

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

// LiteralNode specifies some uninterpreted text.
type LiteralNode struct {
	Code  LiteralCode
	Attrs *Attributes // Optional attributes.
	Text  string
	Kind    LiteralKind
	Attrs   attrs.Attributes // Optional attributes.
	Content []byte
}

// LiteralCode specifies the format that is applied to code inline nodes.
type LiteralCode int
// LiteralKind specifies the format that is applied to code inline nodes.
type LiteralKind int

// Constants for LiteralCode
const (
	_              LiteralCode = iota
	LiteralProg                // Inline program code.
	LiteralKeyb                // Keyboard strokes.
	LiteralOutput              // Sample output.
	_              LiteralKind = iota
	LiteralZettel              // Zettel content
	LiteralProg                // Inline program code
	LiteralInput               // Computer input, e.g. Keyboard strokes
	LiteralOutput              // Computer output
	LiteralComment             // Inline comment
	LiteralHTML                // Inline HTML, e.g. for Markdown
	LiteralMath                // Inline math mode
)

func (rn *LiteralNode) inlineNode() {}
func (*LiteralNode) inlineNode() { /* Just a marker */ }

// Accept a visitor and visit the node.
func (rn *LiteralNode) Accept(v Visitor) { v.VisitLiteral(rn) }
// WalkChildren does nothing.
func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ }

Changes to ast/ref.go.

1
2

3
4

5
6
7
8



9
10
11
12
13
14
15

16

17

18



19
20
21
22

23
24
25



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


41
42
43
44
45
46
47
48





49
50
51
52
53
54
55
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20

21
22
23
24
25
26
27
28

29

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

// Package ast provides the abstract syntax tree.
package ast

import (
	"net/url"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/zettel/id"
)

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

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

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

func localState(path string) (RefState, bool) {
	if len(path) > 0 && path[0] == '/' {
		if len(path) > 1 && path[1] == '/' {
			return RefStateBased, true
		}
		return RefStateHosted, true
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







+
+
+







	return RefStateInvalid, false
}

// String returns the string representation of a reference.
func (r Reference) String() string {
	if r.URL != nil {
		return r.URL.String()
	}
	if r.State == RefStateQuery {
		return QueryPrefix + r.Value
	}
	return r.Value
}

// IsValid returns true if reference is valid
func (r *Reference) IsValid() bool { return r.State != RefStateInvalid }

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

// Package ast_test provides the tests for the abstract syntax tree.
package ast_test

import (
	"testing"

	"zettelstore.de/z/ast"
)

func TestParseReference(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		link string
		err  bool
		exp  string
	}{
		{"", true, ""},
		{"123", false, "123"},
37
38
39
40
41
42
43

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







+







		if got.IsValid() && got.String() != tc.exp {
			t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got)
		}
	}
}

func TestReferenceIsZettelMaterial(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		link       string
		isZettel   bool
		isExternal bool
		isLocal    bool
	}{
		{"", false, false, false},

Deleted ast/traverser.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

































































































































































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

// Package ast provides the abstract syntax tree.
package ast

// A traverser is a Visitor that just traverses the AST and delegates node
// spacific actions to a Visitor. This Visitor should not traverse the AST.

// TopDownTraverser visits first the node and then the children nodes.
type TopDownTraverser struct {
	v Visitor
}

// NewTopDownTraverser creates a new traverser.
func NewTopDownTraverser(visitor Visitor) TopDownTraverser {
	return TopDownTraverser{visitor}
}

// VisitVerbatim has nothing to traverse.
func (t TopDownTraverser) VisitVerbatim(vn *VerbatimNode) { t.v.VisitVerbatim(vn) }

// VisitRegion traverses the content and the additional text.
func (t TopDownTraverser) VisitRegion(rn *RegionNode) {
	t.v.VisitRegion(rn)
	t.VisitBlockSlice(rn.Blocks)
	t.VisitInlineSlice(rn.Inlines)
}

// VisitHeading traverses the heading.
func (t TopDownTraverser) VisitHeading(hn *HeadingNode) {
	t.v.VisitHeading(hn)
	t.VisitInlineSlice(hn.Inlines)
}

// VisitHRule traverses nothing.
func (t TopDownTraverser) VisitHRule(hn *HRuleNode) { t.v.VisitHRule(hn) }

// VisitNestedList traverses all nested list elements.
func (t TopDownTraverser) VisitNestedList(ln *NestedListNode) {
	t.v.VisitNestedList(ln)
	for _, item := range ln.Items {
		t.visitItemSlice(item)
	}
}

// VisitDescriptionList traverses all description terms and their associated
// descriptions.
func (t TopDownTraverser) VisitDescriptionList(dn *DescriptionListNode) {
	t.v.VisitDescriptionList(dn)
	for _, defs := range dn.Descriptions {
		t.VisitInlineSlice(defs.Term)
		for _, descr := range defs.Descriptions {
			t.visitDescriptionSlice(descr)
		}
	}
}

// VisitPara traverses the inlines of a paragraph.
func (t TopDownTraverser) VisitPara(pn *ParaNode) {
	t.v.VisitPara(pn)
	t.VisitInlineSlice(pn.Inlines)
}

// VisitTable traverses all cells of the header and then row-wise all cells of
// the table body.
func (t TopDownTraverser) VisitTable(tn *TableNode) {
	t.v.VisitTable(tn)
	for _, col := range tn.Header {
		t.VisitInlineSlice(col.Inlines)
	}
	for _, row := range tn.Rows {
		for _, col := range row {
			t.VisitInlineSlice(col.Inlines)
		}
	}
}

// VisitBLOB traverses nothing.
func (t TopDownTraverser) VisitBLOB(bn *BLOBNode) { t.v.VisitBLOB(bn) }

// VisitText traverses nothing.
func (t TopDownTraverser) VisitText(tn *TextNode) { t.v.VisitText(tn) }

// VisitTag traverses nothing.
func (t TopDownTraverser) VisitTag(tn *TagNode) { t.v.VisitTag(tn) }

// VisitSpace traverses nothing.
func (t TopDownTraverser) VisitSpace(sn *SpaceNode) { t.v.VisitSpace(sn) }

// VisitBreak traverses nothing.
func (t TopDownTraverser) VisitBreak(bn *BreakNode) { t.v.VisitBreak(bn) }

// VisitLink traverses the link text.
func (t TopDownTraverser) VisitLink(ln *LinkNode) {
	t.v.VisitLink(ln)
	t.VisitInlineSlice(ln.Inlines)
}

// VisitImage traverses the image text.
func (t TopDownTraverser) VisitImage(in *ImageNode) {
	t.v.VisitImage(in)
	t.VisitInlineSlice(in.Inlines)
}

// VisitCite traverses the cite text.
func (t TopDownTraverser) VisitCite(cn *CiteNode) {
	t.v.VisitCite(cn)
	t.VisitInlineSlice(cn.Inlines)
}

// VisitFootnote traverses the footnote text.
func (t TopDownTraverser) VisitFootnote(fn *FootnoteNode) {
	t.v.VisitFootnote(fn)
	t.VisitInlineSlice(fn.Inlines)
}

// VisitMark traverses nothing.
func (t TopDownTraverser) VisitMark(mn *MarkNode) { t.v.VisitMark(mn) }

// VisitFormat traverses the formatted text.
func (t TopDownTraverser) VisitFormat(fn *FormatNode) {
	t.v.VisitFormat(fn)
	t.VisitInlineSlice(fn.Inlines)
}

// VisitLiteral traverses nothing.
func (t TopDownTraverser) VisitLiteral(ln *LiteralNode) { t.v.VisitLiteral(ln) }

// VisitBlockSlice traverses a block slice.
func (t TopDownTraverser) VisitBlockSlice(bns BlockSlice) {
	for _, bn := range bns {
		bn.Accept(t)
	}
}

func (t TopDownTraverser) visitItemSlice(ins ItemSlice) {
	for _, in := range ins {
		in.Accept(t)
	}
}

func (t TopDownTraverser) visitDescriptionSlice(dns DescriptionSlice) {
	for _, dn := range dns {
		dn.Accept(t)
	}
}

// VisitInlineSlice traverses a block slice.
func (t TopDownTraverser) VisitInlineSlice(ins InlineSlice) {
	for _, in := range ins {
		in.Accept(t)
	}
}

Deleted ast/visitor.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package ast provides the abstract syntax tree.
package ast

// Visitor is the interface all visitors must implement.
type Visitor interface {
	// Block nodes
	VisitVerbatim(vn *VerbatimNode)
	VisitRegion(rn *RegionNode)
	VisitHeading(hn *HeadingNode)
	VisitHRule(hn *HRuleNode)
	VisitNestedList(ln *NestedListNode)
	VisitDescriptionList(dn *DescriptionListNode)
	VisitPara(pn *ParaNode)
	VisitTable(tn *TableNode)
	VisitBLOB(bn *BLOBNode)

	// Inline nodes
	VisitText(tn *TextNode)
	VisitTag(tn *TagNode)
	VisitSpace(sn *SpaceNode)
	VisitBreak(bn *BreakNode)
	VisitLink(ln *LinkNode)
	VisitImage(in *ImageNode)
	VisitCite(cn *CiteNode)
	VisitFootnote(fn *FootnoteNode)
	VisitMark(mn *MarkNode)
	VisitFormat(fn *FormatNode)
	VisitLiteral(ln *LiteralNode)
}

Added ast/walk.go.

















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

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

// Walk traverses the AST.
func Walk(v Visitor, node Node) {
	if v = v.Visit(node); v == nil {
		return
	}

	// Implementation note:
	// It is much faster to use interface dispatching than to use a switch statement.
	// On my "cpu: Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz", a switch statement
	// implementation tooks approx 940-980 ns/op. Interface dispatching is in the
	// range of 900-930 ns/op.
	node.WalkChildren(v)
	v.Visit(nil)
}

// WalkItemSlice traverses an item slice.
func WalkItemSlice(v Visitor, ins ItemSlice) {
	for _, in := range ins {
		Walk(v, in)
	}
}

// WalkDescriptionSlice traverses an item slice.
func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) {
	for _, dn := range dns {
		Walk(v, dn)
	}
}

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

package ast_test

import (
	"testing"

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

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

-
+

-
+




+
+
+








-
-
-
+
+
+
-
-
+







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

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

import (
	"time"

	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/place"
	"zettelstore.de/z/web/server"
	"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
}
39
40
41
42
43
44
45
46
47


48
49
50
51
52
53
54
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
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




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







-
+
















-
+

-
+
+
+
+
}

// Manager is the main interface for providing the service.
type Manager interface {
	TokenManager
	AuthzManager

	PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, Policy)
	BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy)
}

// Policy is an interface for checking access authorization.
type Policy interface {
	// User is allowed to create a new zettel.
	CanCreate(user, newMeta *meta.Meta) bool

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

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

import (
	"bytes"

	"golang.org/x/crypto/bcrypt"
	"zettelstore.de/z/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"

	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxreader"
)

var encoding = base64.RawURLEncoding

const digestAlg = crypto.SHA384

func sign(claim sx.Object, secret []byte) ([]byte, error) {
	var buf bytes.Buffer
	_, 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 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

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

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

	"github.com/pascaldekloe/jwt"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
	"zettelstore.de/z/web/server"
	"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
154
155
156
157
158
159
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.
// ErrNoUser signals that the meta data has no role value 'user'.
var ErrNoUser = errors.New("auth: meta is no user")
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) {
	if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser {
		return nil, ErrNoUser
	}
	subject, ok := ident.Get(meta.KeyUserID)
	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.String(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)
	if ident == "" {
		return auth.TokenData{}, ErrNoIdent
		return ErrNoIdent
	}
	if zidS, ok := claims.Set["zid"].(string); ok {
		if zid, err := id.Parse(zidS); err == nil {
	issued := time.Unix(int64(vals[2].(sx.Int64)), 0)
	expires := time.Unix(int64(vals[3].(sx.Int64)), 0)
			if kind, ok := claims.Set["_tk"].(float64); ok {
				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
}
167
168
169
170
171
172
173
174

175
176
177
178
179
180
181
182
183


184
163
164
165
166
167
168
169

170
171
172
173
174
175
176
177


178
179
180







-
+







-
-
+
+

			return meta.UserRoleUnknown
		}
		return meta.UserRoleOwner
	}
	if a.IsOwner(user.Zid) {
		return meta.UserRoleOwner
	}
	if val, ok := user.Get(meta.KeyUserRole); ok {
	if val, ok := user.Get(api.KeyUserRole); ok {
		if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown {
			return ur
		}
	}
	return meta.UserRoleReader
}

func (a *myAuth) PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, auth.Policy) {
	return policy.PlaceWithPolicy(auth, a, unprotectedPlace, rtConfig)
func (a *myAuth) BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) {
	return policy.BoxWithPolicy(a, unprotectedBox, rtConfig)
}

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
1

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

// Package policy provides some interfaces and implementation for authorization policies.
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
}

37
38
39
40
41
42
43







44
45
46
47
48
49
50
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59







+
+
+
+
+
+
+







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
	}
	return ap.pre.CanRefresh(user)
}

func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool {
	if ap.authConfig.GetVisibility(m) == meta.VisibilityExpert {
		return ap.authConfig.GetExpertMode()
	}
	return true
}

Added 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 policy

import (
	"context"

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

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

// polBox implements a policy box.
type polBox struct {
	box    box.Box
	policy auth.Policy
}

// newBox creates a new policy box.
func newBox(box box.Box, policy auth.Policy) box.Box {
	return &polBox{
		box:    box,
		policy: policy,
	}
}

func (pp *polBox) Location() string {
	return pp.box.Location()
}

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

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

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

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

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

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

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

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

func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	zid := zettel.Meta.Zid
	user := server.GetUser(ctx)
	if !zid.IsValid() {
		return box.ErrInvalidZid{Zid: zid.String()}
	}
	// Write existing zettel
	oldZettel, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return err
	}
	if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) {
		return pp.box.UpdateZettel(ctx, zettel)
	}
	return box.NewErrNotAllowed("Write", user, zid)
}

func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	return pp.box.AllowRenameZettel(ctx, zid)
}

func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	z, err := pp.box.GetZettel(ctx, curZid)
	if err != nil {
		return err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanRename(user, z.Meta) {
		return pp.box.RenameZettel(ctx, curZid, newZid)
	}
	return box.NewErrNotAllowed("Rename", user, curZid)
}

func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	return pp.box.CanDeleteZettel(ctx, zid)
}

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

func (pp *polBox) Refresh(ctx context.Context) error {
	user := server.GetUser(ctx)
	if pp.policy.CanRefresh(user) {
		return pp.box.Refresh(ctx)
	}
	return box.NewErrNotAllowed("Refresh", user, id.Invalid)
}
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
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
50
51

52
53

54
55

56
57
58
59
60

-
+

-
+




+
+
+


-



+

-
+






-
-
-
+
+
+





+
+

-
+









-
+




-
+

-
+

-
+




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

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

import (
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

type defaultPolicy struct {
	manager auth.AuthzManager
}

func (d *defaultPolicy) CanCreate(user, newMeta *meta.Meta) bool { return true }
func (d *defaultPolicy) CanRead(user, m *meta.Meta) bool         { return true }
func (d *defaultPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
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(meta.KeyReadOnly)
	metaRo, ok := m.Get(api.KeyReadOnly)
	if !ok {
		return true
	}
	if user == nil {
		// If we are here, there is no authentication.
		// See owner.go:CanWrite.

		// No authentication: check for owner-like restriction, because the user
		// acts as an owner
		return metaRo != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo)
		return metaRo != api.ValueUserRoleOwner && !meta.BoolValue(metaRo)
	}

	userRole := d.manager.GetUserRole(user)
	switch metaRo {
	case meta.ValueUserRoleReader:
	case api.ValueUserRoleReader:
		return userRole > meta.UserRoleReader
	case meta.ValueUserRoleWriter:
	case api.ValueUserRoleWriter:
		return userRole > meta.UserRoleWriter
	case meta.ValueUserRoleOwner:
	case api.ValueUserRoleOwner:
		return userRole > meta.UserRoleOwner
	}
	return !meta.BoolValue(metaRo)
}

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

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

import (
	"zettelstore.de/client.fossil/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
}

func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanCreate(user, newMeta) {
		return false
	}
	return o.userIsOwner(user) || o.userCanCreate(user, newMeta)
}

func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool {
	if o.manager.GetUserRole(user) == meta.UserRoleReader {
		return false
	}
	if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
	if _, ok := newMeta.Get(api.KeyUserID); ok {
		return false
	}
	return true
}

func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool {
	// No need to call o.pre.CanRead(user, meta), because it will always return true.
56
57
58
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
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







-
+



+
+
-
+
+
+
+
+
+



-
-
-
-
+
+
+
+
















-
+









-
+
+







		return false
	case meta.VisibilityPublic:
		return true
	}
	if user == nil {
		return false
	}
	if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
	if _, ok := m.Get(api.KeyUserID); ok {
		// Only the user can read its own zettel
		return user.Zid == m.Zid
	}
	switch o.manager.GetUserRole(user) {
	case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner:
	return true
		return true
	case meta.UserRoleCreator:
		return vis == meta.VisibilityCreator
	default:
		return false
	}
}

var noChangeUser = []string{
	meta.KeyID,
	meta.KeyRole,
	meta.KeyUserID,
	meta.KeyUserRole,
	api.KeyID,
	api.KeyRole,
	api.KeyUserID,
	api.KeyUserRole,
}

func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) {
		return false
	}
	vis := o.authConfig.GetVisibility(oldMeta)
	if res, ok := o.checkVisibility(user, vis); ok {
		return res
	}
	if o.userIsOwner(user) {
		return true
	}
	if !o.userCanRead(user, oldMeta, vis) {
		return false
	}
	if role, ok := oldMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
	if _, ok := oldMeta.Get(api.KeyUserID); ok {
		// Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and
		// user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid)
		for _, key := range noChangeUser {
			if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") {
				return false
			}
		}
		return true
	}
	if o.manager.GetUserRole(user) == meta.UserRoleReader {
	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) {
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
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







+
+
+
+
+
+
+
+
+
+















-
+




		return false
	}
	if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok {
		return res
	}
	return o.userIsOwner(user)
}

func (o *ownerPolicy) CanRefresh(user *meta.Meta) bool {
	switch userRole := o.manager.GetUserRole(user); userRole {
	case meta.UserRoleUnknown:
		return o.authConfig.GetSimpleMode()
	case meta.UserRoleCreator:
		return o.authConfig.GetExpertMode() || o.authConfig.GetSimpleMode()
	}
	return true
}

func (o *ownerPolicy) checkVisibility(user *meta.Meta, vis meta.Visibility) (bool, bool) {
	if vis == meta.VisibilityExpert {
		return o.userIsOwner(user) && o.authConfig.GetExpertMode(), true
	}
	return false, false
}

func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool {
	if user == nil {
		return false
	}
	if o.manager.IsOwner(user.Zid) {
		return true
	}
	if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner {
	if val, ok := user.Get(api.KeyUserRole); ok && val == api.ValueUserRoleOwner {
		return true
	}
	return false
}

Deleted auth/policy/place.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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





































































































































































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

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

import (
	"context"
	"io"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
	"zettelstore.de/z/web/server"
)

// PlaceWithPolicy wraps the given place inside a policy place.
func PlaceWithPolicy(
	auth server.Auth,
	manager auth.AuthzManager,
	place place.Place,
	authConfig config.AuthConfig,
) (place.Place, auth.Policy) {
	pol := newPolicy(manager, authConfig)
	return newPlace(auth, place, pol), pol
}

// polPlace implements a policy place.
type polPlace struct {
	auth   server.Auth
	place  place.Place
	policy auth.Policy
}

// newPlace creates a new policy place.
func newPlace(auth server.Auth, place place.Place, policy auth.Policy) place.Place {
	return &polPlace{
		auth:   auth,
		place:  place,
		policy: policy,
	}
}

func (pp *polPlace) Location() string {
	return pp.place.Location()
}

func (pp *polPlace) CanCreateZettel(ctx context.Context) bool {
	return pp.place.CanCreateZettel(ctx)
}

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

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

func (pp *polPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	m, err := pp.place.GetMeta(ctx, zid)
	if err != nil {
		return nil, err
	}
	user := pp.auth.GetUser(ctx)
	if pp.policy.CanRead(user, m) {
		return m, nil
	}
	return nil, place.NewErrNotAllowed("GetMeta", user, zid)
}

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

func (pp *polPlace) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
	user := pp.auth.GetUser(ctx)
	canRead := pp.policy.CanRead
	s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
	return pp.place.SelectMeta(ctx, s)
}

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

func (pp *polPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	zid := zettel.Meta.Zid
	user := pp.auth.GetUser(ctx)
	if !zid.IsValid() {
		return &place.ErrInvalidID{Zid: zid}
	}
	// Write existing zettel
	oldMeta, err := pp.place.GetMeta(ctx, zid)
	if err != nil {
		return err
	}
	if pp.policy.CanWrite(user, oldMeta, zettel.Meta) {
		return pp.place.UpdateZettel(ctx, zettel)
	}
	return place.NewErrNotAllowed("Write", user, zid)
}

func (pp *polPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	return pp.place.AllowRenameZettel(ctx, zid)
}

func (pp *polPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	meta, err := pp.place.GetMeta(ctx, curZid)
	if err != nil {
		return err
	}
	user := pp.auth.GetUser(ctx)
	if pp.policy.CanRename(user, meta) {
		return pp.place.RenameZettel(ctx, curZid, newZid)
	}
	return place.NewErrNotAllowed("Rename", user, curZid)
}

func (pp *polPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	return pp.place.CanDeleteZettel(ctx, zid)
}

func (pp *polPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	meta, err := pp.place.GetMeta(ctx, zid)
	if err != nil {
		return err
	}
	user := pp.auth.GetUser(ctx)
	if pp.policy.CanDelete(user, meta) {
		return pp.place.DeleteZettel(ctx, zid)
	}
	return place.NewErrNotAllowed("Delete", user, zid)
}

func (pp *polPlace) ReadStats(st *place.Stats) {
	pp.place.ReadStats(st)
}

func (pp *polPlace) Dump(w io.Writer) {
	pp.place.Dump(w)
}

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

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

import (
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/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{}
60
61
62
63
64
65
66




63
64
65
66
67
68
69
70
71
72
73







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

28
29
30
31
32
33
34
35
36
















37
38
39
40


41
42
43
44
45




46
47
48


49
50
51

52
53
54
55
56
57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21


22
23
24
25
26
27
28
29
30
31
32
33








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


52
53





54
55
56
57
58


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
712
713
714
715
716


717
718
719
720

-
+

-
+




+
+
+


-






+

-
-
+
+



+




+

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


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

-
-
+
+



+









-
-
-
+
+
+













-
+







-
+

+


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




-
+


+













+






+






+















-
+


+






+











+






+






+




+
+
+
+
+
+
+


+






+






+






+





+




















+











-
+














+






+






+






+






+






+






+






+






+






+





+








+






+






+






+






+


















+



















+






+






+






+






+






+






+






+


















+



















+






+






+






+






+






+






+






+















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

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



+
+
+
+
+
+
+


-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
+




-
-
+
+
+
+
+
+
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+


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

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

import (
	"fmt"
	"testing"

	"zettelstore.de/client.fossil/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
		expert   bool
		simple   bool
	}{
		{true, true, true},
		{true, true, false},
		{true, false, true},
		{true, false, false},
		{false, true, true},
		{false, true, false},
		{false, false, true},
		{false, false, false},
		{true, true, true, true},
		{true, true, true, false},
		{true, true, false, true},
		{true, true, false, false},
		{true, false, true, true},
		{true, false, true, false},
		{true, false, false, true},
		{true, false, false, false},
		{false, true, true, true},
		{false, true, true, false},
		{false, true, false, true},
		{false, true, false, false},
		{false, false, true, true},
		{false, false, true, false},
		{false, false, false, true},
		{false, false, false, false},
	}
	for _, ts := range testScene {
		authzManager := &testAuthzManager{
			readOnly: ts.readonly,
		pol := newPolicy(
			&testAuthzManager{readOnly: ts.readonly, withAuth: ts.withAuth},
			withAuth: ts.withAuth,
		}
		pol := newPolicy(authzManager, &authConfig{ts.expert})
		name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v",
			ts.readonly, ts.withAuth, ts.expert)
			&authConfig{simple: ts.simple, expert: ts.expert},
		)
		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, ts.expert)
			testRead(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			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 {
	readOnly bool
	withAuth bool
}

func (a *testAuthzManager) IsReadonly() bool        { return a.readOnly }
func (a *testAuthzManager) Owner() id.Zid           { return ownerZid }
func (a *testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid }
func (a *testAuthzManager) IsReadonly() bool      { return a.readOnly }
func (*testAuthzManager) Owner() id.Zid           { return ownerZid }
func (*testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid }

func (a *testAuthzManager) WithAuth() bool { return a.withAuth }

func (a *testAuthzManager) GetUserRole(user *meta.Meta) meta.UserRole {
	if user == nil {
		if a.WithAuth() {
			return meta.UserRoleUnknown
		}
		return meta.UserRoleOwner
	}
	if a.IsOwner(user.Zid) {
		return meta.UserRoleOwner
	}
	if val, ok := user.Get(meta.KeyUserRole); ok {
	if val, ok := user.Get(api.KeyUserRole); ok {
		if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown {
			return ur
		}
	}
	return meta.UserRoleReader
}

type authConfig struct{ expert bool }
type authConfig struct{ simple, expert bool }

func (ac *authConfig) GetSimpleMode() bool { return ac.simple }
func (ac *authConfig) GetExpertMode() bool { return ac.expert }

func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility {
	if vis, ok := m.Get(meta.KeyVisibility); ok {
func (*authConfig) GetVisibility(m *meta.Meta) meta.Visibility {
	if vis, ok := m.Get(api.KeyVisibility); ok {
		switch vis {
		case meta.ValueVisibilityPublic:
			return meta.VisibilityPublic
		case meta.ValueVisibilityOwner:
			return meta.VisibilityOwner
		return meta.GetVisibility(vis)
		case meta.ValueVisibilityExpert:
			return meta.VisibilityExpert
		}
	}
	return meta.VisibilityLogin
}

func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly, isExpert bool) {
func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly bool) {
	t.Helper()
	anonUser := newAnon()
	creator := newCreator()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()
	userZettel := newUserZettel()
	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},
		// Ordinary zettel
		{anonUser, zettel, !withAuth && !readonly},
		{creator, zettel, !readonly},
		{reader, zettel, !withAuth && !readonly},
		{writer, zettel, !readonly},
		{owner, zettel, !readonly},
		{owner2, zettel, !readonly},
		// User zettel
		{anonUser, userZettel, !withAuth && !readonly},
		{creator, userZettel, !withAuth && !readonly},
		{reader, userZettel, !withAuth && !readonly},
		{writer, userZettel, !withAuth && !readonly},
		{owner, userZettel, !readonly},
		{owner2, userZettel, !readonly},
	}
	for _, tc := range testCases {
		t.Run("Create", func(tt *testing.T) {
			got := pol.CanCreate(tc.user, tc.meta)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testRead(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
func testRead(t *testing.T, pol auth.Policy, withAuth, expert bool) {
	t.Helper()
	anonUser := newAnon()
	creator := newCreator()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()
	publicZettel := newPublicZettel()
	creatorZettel := newCreatorZettel()
	loginZettel := newLoginZettel()
	ownerZettel := newOwnerZettel()
	expertZettel := newExpertZettel()
	userZettel := newUserZettel()
	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},
		// Ordinary zettel
		{anonUser, zettel, !withAuth},
		{creator, zettel, !withAuth},
		{reader, zettel, true},
		{writer, zettel, true},
		{owner, zettel, true},
		{owner2, zettel, true},
		// Public zettel
		{anonUser, publicZettel, true},
		{creator, publicZettel, true},
		{reader, publicZettel, true},
		{writer, publicZettel, true},
		{owner, publicZettel, true},
		{owner2, publicZettel, true},
		// Creator zettel
		{anonUser, creatorZettel, !withAuth},
		{creator, creatorZettel, true},
		{reader, creatorZettel, true},
		{writer, creatorZettel, true},
		{owner, creatorZettel, true},
		{owner2, creatorZettel, true},
		// Login zettel
		{anonUser, loginZettel, !withAuth},
		{creator, loginZettel, !withAuth},
		{reader, loginZettel, true},
		{writer, loginZettel, true},
		{owner, loginZettel, true},
		{owner2, loginZettel, true},
		// Owner zettel
		{anonUser, ownerZettel, !withAuth},
		{creator, ownerZettel, !withAuth},
		{reader, ownerZettel, !withAuth},
		{writer, ownerZettel, !withAuth},
		{owner, ownerZettel, true},
		{owner2, ownerZettel, true},
		// Expert zettel
		{anonUser, expertZettel, !withAuth && expert},
		{creator, expertZettel, !withAuth && expert},
		{reader, expertZettel, !withAuth && expert},
		{writer, expertZettel, !withAuth && expert},
		{owner, expertZettel, expert},
		{owner2, expertZettel, expert},
		// Other user zettel
		{anonUser, userZettel, !withAuth},
		{creator, userZettel, !withAuth},
		{reader, userZettel, !withAuth},
		{writer, userZettel, !withAuth},
		{owner, userZettel, true},
		{owner2, userZettel, true},
		// Own user zettel
		{creator, creator, true},
		{reader, reader, true},
		{writer, writer, true},
		{owner, owner, true},
		{owner, owner2, true},
		{owner2, owner, true},
		{owner2, owner2, true},
	}
	for _, tc := range testCases {
		t.Run("Read", func(tt *testing.T) {
			got := pol.CanRead(tc.user, tc.meta)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testWrite(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()
	publicZettel := newPublicZettel()
	loginZettel := newLoginZettel()
	ownerZettel := newOwnerZettel()
	expertZettel := newExpertZettel()
	userZettel := newUserZettel()
	writerNew := writer.Clone()
	writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, ""))
	writerNew.Set(api.KeyUserRole, owner.GetDefault(api.KeyUserRole, ""))
	roFalse := newRoFalseZettel()
	roTrue := newRoTrueZettel()
	roReader := newRoReaderZettel()
	roWriter := newRoWriterZettel()
	roOwner := newRoOwnerZettel()
	notAuthNotReadonly := !withAuth && !readonly
	testCases := []struct {
		user *meta.Meta
		old  *meta.Meta
		new  *meta.Meta
		exp  bool
	}{
		// No old and new meta
		{anonUser, nil, nil, false},
		{creator, nil, nil, false},
		{reader, nil, nil, false},
		{writer, nil, nil, false},
		{owner, nil, nil, false},
		{owner2, nil, nil, false},
		// No old meta
		{anonUser, nil, zettel, false},
		{creator, nil, zettel, false},
		{reader, nil, zettel, false},
		{writer, nil, zettel, false},
		{owner, nil, zettel, false},
		{owner2, nil, zettel, false},
		// No new meta
		{anonUser, zettel, nil, false},
		{creator, zettel, nil, false},
		{reader, zettel, nil, false},
		{writer, zettel, nil, false},
		{owner, zettel, nil, false},
		{owner2, zettel, nil, false},
		// Old an new zettel have different zettel identifier
		{anonUser, zettel, publicZettel, false},
		{creator, zettel, publicZettel, false},
		{reader, zettel, publicZettel, false},
		{writer, zettel, publicZettel, false},
		{owner, zettel, publicZettel, false},
		{owner2, zettel, publicZettel, false},
		// Overwrite a normal zettel
		{anonUser, zettel, zettel, notAuthNotReadonly},
		{creator, zettel, zettel, notAuthNotReadonly},
		{reader, zettel, zettel, notAuthNotReadonly},
		{writer, zettel, zettel, !readonly},
		{owner, zettel, zettel, !readonly},
		{owner2, zettel, zettel, !readonly},
		// Public zettel
		{anonUser, publicZettel, publicZettel, notAuthNotReadonly},
		{creator, publicZettel, publicZettel, notAuthNotReadonly},
		{reader, publicZettel, publicZettel, notAuthNotReadonly},
		{writer, publicZettel, publicZettel, !readonly},
		{owner, publicZettel, publicZettel, !readonly},
		{owner2, publicZettel, publicZettel, !readonly},
		// Login zettel
		{anonUser, loginZettel, loginZettel, notAuthNotReadonly},
		{creator, loginZettel, loginZettel, notAuthNotReadonly},
		{reader, loginZettel, loginZettel, notAuthNotReadonly},
		{writer, loginZettel, loginZettel, !readonly},
		{owner, loginZettel, loginZettel, !readonly},
		{owner2, loginZettel, loginZettel, !readonly},
		// Owner zettel
		{anonUser, ownerZettel, ownerZettel, notAuthNotReadonly},
		{creator, ownerZettel, ownerZettel, notAuthNotReadonly},
		{reader, ownerZettel, ownerZettel, notAuthNotReadonly},
		{writer, ownerZettel, ownerZettel, notAuthNotReadonly},
		{owner, ownerZettel, ownerZettel, !readonly},
		{owner2, ownerZettel, ownerZettel, !readonly},
		// Expert zettel
		{anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert},
		{creator, expertZettel, expertZettel, notAuthNotReadonly && expert},
		{reader, expertZettel, expertZettel, notAuthNotReadonly && expert},
		{writer, expertZettel, expertZettel, notAuthNotReadonly && expert},
		{owner, expertZettel, expertZettel, !readonly && expert},
		{owner2, expertZettel, expertZettel, !readonly && expert},
		// Other user zettel
		{anonUser, userZettel, userZettel, notAuthNotReadonly},
		{creator, userZettel, userZettel, notAuthNotReadonly},
		{reader, userZettel, userZettel, notAuthNotReadonly},
		{writer, userZettel, userZettel, notAuthNotReadonly},
		{owner, userZettel, userZettel, !readonly},
		{owner2, userZettel, userZettel, !readonly},
		// Own user zettel
		{creator, creator, creator, !readonly},
		{reader, reader, reader, !readonly},
		{writer, writer, writer, !readonly},
		{owner, owner, owner, !readonly},
		{owner2, owner2, owner2, !readonly},
		// Writer cannot change importand metadata of its own user zettel
		{writer, writer, writerNew, notAuthNotReadonly},
		// No r/o zettel
		{anonUser, roFalse, roFalse, notAuthNotReadonly},
		{creator, roFalse, roFalse, notAuthNotReadonly},
		{reader, roFalse, roFalse, notAuthNotReadonly},
		{writer, roFalse, roFalse, !readonly},
		{owner, roFalse, roFalse, !readonly},
		{owner2, roFalse, roFalse, !readonly},
		// Reader r/o zettel
		{anonUser, roReader, roReader, false},
		{creator, roReader, roReader, false},
		{reader, roReader, roReader, false},
		{writer, roReader, roReader, !readonly},
		{owner, roReader, roReader, !readonly},
		{owner2, roReader, roReader, !readonly},
		// Writer r/o zettel
		{anonUser, roWriter, roWriter, false},
		{creator, roWriter, roWriter, false},
		{reader, roWriter, roWriter, false},
		{writer, roWriter, roWriter, false},
		{owner, roWriter, roWriter, !readonly},
		{owner2, roWriter, roWriter, !readonly},
		// Owner r/o zettel
		{anonUser, roOwner, roOwner, false},
		{creator, roOwner, roOwner, false},
		{reader, roOwner, roOwner, false},
		{writer, roOwner, roOwner, false},
		{owner, roOwner, roOwner, false},
		{owner2, roOwner, roOwner, false},
		// r/o = true zettel
		{anonUser, roTrue, roTrue, false},
		{creator, roTrue, roTrue, false},
		{reader, roTrue, roTrue, false},
		{writer, roTrue, roTrue, false},
		{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) {
	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("Delete", func(tt *testing.T) {
			got := pol.CanDelete(tc.user, tc.meta)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testRefresh(t *testing.T, pol auth.Policy, withAuth, expert, simple bool) {
	t.Helper()
	testCases := []struct {
		user *meta.Meta
		exp  bool
	}{
		{newAnon(), (!withAuth && expert) || simple},
		{newCreator(), !withAuth || expert || simple},
		{newReader(), true},
		{newWriter(), true},
		{newOwner(), true},
		{newOwner2(), true},
	}
	for _, tc := range testCases {
		t.Run("Refresh", func(tt *testing.T) {
			got := pol.CanRefresh(tc.user)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

const (
	readerZid = id.Zid(1013)
	writerZid = id.Zid(1015)
	ownerZid  = id.Zid(1017)
	owner2Zid = id.Zid(1019)
	zettelZid = id.Zid(1021)
	visZid    = id.Zid(1023)
	userZid   = id.Zid(1025)
	creatorZid = id.Zid(1013)
	readerZid  = id.Zid(1013)
	writerZid  = id.Zid(1015)
	ownerZid   = id.Zid(1017)
	owner2Zid  = id.Zid(1019)
	zettelZid  = id.Zid(1021)
	visZid     = id.Zid(1023)
	userZid    = id.Zid(1025)
)

func newAnon() *meta.Meta { return nil }
func newCreator() *meta.Meta {
	user := meta.New(creatorZid)
	user.Set(api.KeyTitle, "Creator")
	user.Set(api.KeyUserID, "ceator")
	user.Set(api.KeyUserRole, api.ValueUserRoleCreator)
	return user
}
func newReader() *meta.Meta {
	user := meta.New(readerZid)
	user.Set(meta.KeyTitle, "Reader")
	user.Set(meta.KeyRole, meta.ValueRoleUser)
	user.Set(meta.KeyUserRole, meta.ValueUserRoleReader)
	user.Set(api.KeyTitle, "Reader")
	user.Set(api.KeyUserID, "reader")
	user.Set(api.KeyUserRole, api.ValueUserRoleReader)
	return user
}
func newWriter() *meta.Meta {
	user := meta.New(writerZid)
	user.Set(meta.KeyTitle, "Writer")
	user.Set(meta.KeyRole, meta.ValueRoleUser)
	user.Set(meta.KeyUserRole, meta.ValueUserRoleWriter)
	user.Set(api.KeyTitle, "Writer")
	user.Set(api.KeyUserID, "writer")
	user.Set(api.KeyUserRole, api.ValueUserRoleWriter)
	return user
}
func newOwner() *meta.Meta {
	user := meta.New(ownerZid)
	user.Set(meta.KeyTitle, "Owner")
	user.Set(meta.KeyRole, meta.ValueRoleUser)
	user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner)
	user.Set(api.KeyTitle, "Owner")
	user.Set(api.KeyUserID, "owner")
	user.Set(api.KeyUserRole, api.ValueUserRoleOwner)
	return user
}
func newOwner2() *meta.Meta {
	user := meta.New(owner2Zid)
	user.Set(meta.KeyTitle, "Owner 2")
	user.Set(meta.KeyRole, meta.ValueRoleUser)
	user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner)
	user.Set(api.KeyTitle, "Owner 2")
	user.Set(api.KeyUserID, "owner-2")
	user.Set(api.KeyUserRole, api.ValueUserRoleOwner)
	return user
}
func newZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Any Zettel")
	m.Set(api.KeyTitle, "Any Zettel")
	return m
}
func newPublicZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Public Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
	m.Set(api.KeyTitle, "Public Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityPublic)
	return m
}
func newCreatorZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(api.KeyTitle, "Creator Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityCreator)
	return m
}
func newLoginZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Login Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin)
	m.Set(api.KeyTitle, "Login Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}
func newOwnerZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Owner Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner)
	m.Set(api.KeyTitle, "Owner Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityOwner)
	return m
}
func newExpertZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Expert Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
	m.Set(api.KeyTitle, "Expert Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m
}
func newRoFalseZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "No r/o Zettel")
	m.Set(meta.KeyReadOnly, "false")
	m.Set(api.KeyTitle, "No r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueFalse)
	return m
}
func newRoTrueZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "A r/o Zettel")
	m.Set(meta.KeyReadOnly, "true")
	m.Set(api.KeyTitle, "A r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueTrue)
	return m
}
func newRoReaderZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Reader r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueUserRoleReader)
	m.Set(api.KeyTitle, "Reader r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueUserRoleReader)
	return m
}
func newRoWriterZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Writer r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter)
	m.Set(api.KeyTitle, "Writer r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueUserRoleWriter)
	return m
}
func newRoOwnerZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Owner r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueUserRoleOwner)
	m.Set(api.KeyTitle, "Owner r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueUserRoleOwner)
	return m
}
func newUserZettel() *meta.Meta {
	m := meta.New(userZid)
	m.Set(meta.KeyTitle, "Any User")
	m.Set(meta.KeyRole, meta.ValueRoleUser)
	m.Set(api.KeyTitle, "Any User")
	m.Set(api.KeyUserID, "any")
	return m
}

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
25

-
+

-
+




+
+
+


-


-
+



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

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

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

type roPolicy struct{}

func (p *roPolicy) CanCreate(user, newMeta *meta.Meta) bool         { return false }
func (p *roPolicy) CanRead(user, m *meta.Meta) bool                 { return true }
func (p *roPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return false }
func (p *roPolicy) CanRename(user, m *meta.Meta) bool               { return false }
func (p *roPolicy) CanDelete(user, m *meta.Meta) bool               { return false }
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 }

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

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

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

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

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

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

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

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

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

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

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

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

// ManagedBox is the interface of managed boxes.
type ManagedBox interface {
	BaseBox

	// HasZettel returns true, if box conains zettel with given identifier.
	HasZettel(context.Context, id.Zid) bool

	// Apply identifier of every zettel to the given function, if predicate returns true.
	ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error

	// Apply metadata of every zettel to the given function, if predicate returns true.
	ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error

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

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

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

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

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

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

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

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

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

// Box is to be used outside the box package and its descendants.
type Box interface {
	BaseBox
	WriteBox

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

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

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

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

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

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

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

	// NumManagedBoxes is the number of boxes managed.
	NumManagedBoxes int

	// Zettel is the number of zettel managed by the box, including
	// duplicates across managed boxes.
	ZettelTotal int

	// LastReload stores the timestamp when a full re-index was done.
	LastReload time.Time

	// DurLastReload is the duration of the last full re-index run.
	DurLastReload time.Duration

	// IndexesSinceReload counts indexing a zettel since the full re-index.
	IndexesSinceReload uint64

	// ZettelIndexed is the number of zettel managed by the indexer.
	ZettelIndexed int

	// IndexUpdates count the number of metadata updates.
	IndexUpdates uint64

	// IndexedWords count the different words indexed.
	IndexedWords uint64

	// IndexedUrls count the different URLs indexed.
	IndexedUrls uint64
}

// Manager is a box-managing box.
type Manager interface {
	Box
	StartStopper
	Subject

	// ReadStats populates st with box statistics
	ReadStats(st *Stats)

	// Dump internal data to a Writer.
	Dump(w io.Writer)
}

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

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

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

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

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

// Enricher is used to update metadata by adding new properties.
type Enricher interface {
	// Enrich computes additional properties and updates the given metadata.
	// It is typically called by zettel reading methods.
	Enrich(ctx context.Context, m *meta.Meta, boxNumber int)
}

// NoEnrichContext will signal an enricher that nothing has to be done.
// This is useful for an Indexer, but also for some box.Box calls, when
// just the plain metadata is needed.
func NoEnrichContext(ctx context.Context) context.Context {
	return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
}

type ctxNoEnrichType struct{}

var ctxNoEnrichKey ctxNoEnrichType

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

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

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

// NewErrNotAllowed creates an new authorization error.
func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error {
	return &ErrNotAllowed{
		Op:   op,
		User: user,
		Zid:  zid,
	}
}

func (err *ErrNotAllowed) Error() string {
	if err.User == nil {
		if err.Zid.IsValid() {
			return fmt.Sprintf(
				"operation %q on zettel %v not allowed for not authorized user",
				err.Op, err.Zid)
		}
		return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op)
	}
	if err.Zid.IsValid() {
		return fmt.Sprintf(
			"operation %q on zettel %v not allowed for user %v/%v",
			err.Op, err.Zid, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid)
	}
	return fmt.Sprintf(
		"operation %q not allowed for user %v/%v",
		err.Op, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid)
}

// Is return true, if the error is of type ErrNotAllowed.
func (*ErrNotAllowed) Is(error) bool { return true }

// ErrStarted is returned when trying to start an already started box.
var ErrStarted = errors.New("box is already started")

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

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

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

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

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

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

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

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

Added 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
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-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European 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/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

var myConfig *meta.Meta
var myZettel = map[id.Zid]struct {
	meta    func(id.Zid) *meta.Meta
	content func(*meta.Meta) []byte
}{
	id.MustParse(api.ZidVersion):              {genVersionBuildM, genVersionBuildC},
	id.MustParse(api.ZidHost):                 {genVersionHostM, genVersionHostC},
	id.MustParse(api.ZidOperatingSystem):      {genVersionOSM, genVersionOSC},
	id.MustParse(api.ZidLog):                  {genLogM, genLogC},
	id.MustParse(api.ZidMemory):               {genMemoryM, genMemoryC},
	id.MustParse(api.ZidBoxManager):           {genManagerM, genManagerC},
	id.MustParse(api.ZidMetadataKey):          {genKeysM, genKeysC},
	id.MustParse(api.ZidParser):               {genParserM, genParserC},
	id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC},
}

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

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

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

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

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

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

func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta")
	for zid, gen := range myZettel {
		if !constraint(zid) {
			continue
		}
		if genMeta := gen.meta; genMeta != nil {
			if m := genMeta(zid); m != nil {
				updateMeta(m)
				cb.enricher.Enrich(ctx, m, cb.number)
				handle(m)
			}
		}
	}
	return nil
}

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

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

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

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

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

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

Added 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func genConfigZettelM(zid id.Zid) *meta.Meta {
	if myConfig == nil {
		return nil
	}
	m := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Startup Configuration")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m
}

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

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

package compbox

import (
	"bytes"
	"fmt"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

func genKeysC(*meta.Meta) []byte {
	keys := meta.GetSortedKeyDescriptions()
	var buf bytes.Buffer
	buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n")
	for _, kd := range keys {
		fmt.Fprintf(&buf,
			"|[[%v|query:%v?]]|%v|%v|%v\n", kd.Name, kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty())
	}
	return buf.Bytes()
}

Added 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

func genLogC(*meta.Meta) []byte {
	const tsFormat = "2006-01-02 15:04:05.999999"
	entries := kernel.Main.RetrieveLogEntries()
	var buf bytes.Buffer
	for _, entry := range entries {
		ts := entry.TS.Format(tsFormat)
		buf.WriteString(ts)
		for j := len(ts); j < len(tsFormat); j++ {
			buf.WriteByte('0')
		}
		buf.WriteByte(' ')
		buf.WriteString(entry.Level.Format())
		buf.WriteByte(' ')
		buf.WriteString(entry.Prefix)
		buf.WriteByte(' ')
		buf.WriteString(entry.Message)
		buf.WriteByte('\n')
	}
	return buf.Bytes()
}

Added 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
38
39
40
41
42
43
44
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"fmt"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

package compbox

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func genMemoryM(zid id.Zid) *meta.Meta {
	if myConfig == nil {
		return nil
	}
	m := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Memory")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m
}

func genMemoryC(*meta.Meta) []byte {
	pageSize := os.Getpagesize()
	var m runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&m)

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

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

package compbox

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

Added 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
59
60
61
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 (
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

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

Added box/constbox/base.css.

































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
/*-----------------------------------------------------------------------------
 * 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;
    line-height: 1.4;
    background-color: #f8f8f8 ;
    height: 100%;
  }
  nav.zs-menu {
    background-color: hsl(210, 28%, 90%);
    overflow: auto;
    white-space: nowrap;
    font-family: sans-serif;
    padding-left: .5rem;
  }
  nav.zs-menu > a {
    float:left;
    display: block;
    text-align: center;
    padding:.41rem .5rem;
    text-decoration: none;
    color:black;
  }
  nav.zs-menu > a:hover, .zs-dropdown:hover button { background-color: hsl(210, 28%, 80%) }
  nav.zs-menu form { float: right }
  nav.zs-menu form input[type=text] {
    padding: .12rem;
    border: none;
    margin-top: .25rem;
    margin-right: .5rem;
  }
  .zs-dropdown {
    float: left;
    overflow: hidden;
  }
  .zs-dropdown > button {
    font-size: 16px;
    border: none;
    outline: none;
    color: black;
    padding:.41rem .5rem;
    background-color: inherit;
    font-family: inherit;
    margin: 0;
  }
  .zs-dropdown-content {
    display: none;
    position: absolute;
    background-color: #f9f9f9;
    min-width: 160px;
    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
    z-index: 1;
  }
  .zs-dropdown-content > a {
    float: none;
    color: black;
    padding:.41rem .5rem;
    text-decoration: none;
    display: block;
    text-align: left;
  }
  .zs-dropdown-content > a:hover { background-color: hsl(210, 28%, 75%) }
  .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 }
  p.zs-meta-zettel { margin-top: .5rem; margin-left: 0.5rem }
  li,figure,figcaption,dl { margin: 0 }
  dt { margin: .5rem 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5rem 0 0 2rem }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: 0.5rem solid lightgray;
    padding-left: 1rem;
    margin-left: 1rem;
    margin-right: 2rem;
    font-style: italic;
  }
  blockquote p { margin-bottom: .5rem }
  blockquote cite { font-style: normal }
  table {
    border-collapse: collapse;
    border-spacing: 0;
    max-width: 100%;
  }
  thead>tr>td { border-bottom: 2px solid hsl(0, 0%, 70%); font-weight: bold }
  tfoot>tr>td { border-top: 2px solid hsl(0, 0%, 70%); font-weight: bold }
  td {
    text-align: left;
    padding: .25rem .5rem;
    border-bottom: 1px solid hsl(0, 0%, 85%)
  }
  main form {
    padding: 0 .5em;
    margin: .5em 0 0 0;
  }
  main form:after {
    content: ".";
    display: block;
    height: 0;
    clear: both;
    visibility: hidden;
  }
  main form div { margin: .5em 0 0 0 }
  input { font-family: monospace }
  input[type="submit"],button,select { font: inherit }
  label { font-family: sans-serif; font-size:.9rem }
  textarea {
    font-family: monospace;
    resize: vertical;
    width: 100%;
  }
  .zs-input {
    padding: .5em;
    display:block;
    border:none;
    border-bottom:1px solid #ccc;
    width:100%;
  }
  input.zs-primary { float:right }
  input.zs-secondary { float:left }
  input.zs-upload {
    padding-left: 1em;
    padding-right: 1em;
  }
  a:not([class]) { text-decoration-skip-ink: auto }
  a.broken { text-decoration: line-through }
  a.external::after { content: "➚"; display: inline-block }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1rem .2rem;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25rem;
  }
  pre {
    padding: .5rem .7rem;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5rem;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5rem .7rem;
    max-width: 100%;
    border-radius: .5rem;
    border: 1px solid black;
  }
  div.zs-indication p:first-child { margin-top: 0 }
  span.zs-indication {
    border: 1px solid black;
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 95%;
  }
  .zs-info {
    background-color: lightblue;
    padding: .5rem 1rem;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5rem 1rem;
  }
  .zs-error {
    background-color: lightpink;
    border-style: none !important;
    font-weight: bold;
  }
  td.left { text-align:left }
  td.center { text-align:center }
  td.right { text-align:right }
  .zs-font-size-0 { font-size:75% }
  .zs-font-size-1 { font-size:83% }
  .zs-font-size-2 { font-size:100% }
  .zs-font-size-3 { font-size:117% }
  .zs-font-size-4 { font-size:150% }
  .zs-font-size-5 { font-size:200% }
  .zs-deprecated { border-style: dashed; padding: .2rem }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1rem;
  }
  .zs-meta a { color:#444 }
  h1+.zs-meta { margin-top:-1rem }
  nav > details { margin-top:1rem }
  details > summary {
    width: 100%;
    background-color: #eee;
    font-family:sans-serif;
  }
  details > ul {
    margin-top:0;
    padding-left:2rem;
    background-color: #eee;
  }
  footer { padding: 0 1rem }
  @media (prefers-reduced-motion: reduce) {
    * {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }

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

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

type constHeader map[string]string

type constZettel struct {
	header  constHeader
	content zettel.Content
}

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

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

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

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

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

func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyMeta")
	for zid, zettel := range cb.zettel {
		if constraint(zid) {
			m := meta.NewWithData(zid, zettel.header)
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

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

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

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

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

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

var constZettelMap = map[id.Zid]constZettel{
	id.ConfigurationZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Runtime Configuration",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxNone,
			api.KeyCreated:    "20200804111624",
			api.KeyVisibility: api.ValueVisibilityOwner,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidLicense): {
		constHeader{
			api.KeyTitle:      "Zettelstore License",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxText,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyModified:   "20220131153422",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentLicense)},
	id.MustParse(api.ZidAuthors): {
		constHeader{
			api.KeyTitle:      "Zettelstore Contributors",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentContributors)},
	id.MustParse(api.ZidDependencies): {
		constHeader{
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20230601163100",
		},
		zettel.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155100",
			api.KeyModified:   "20240219145300",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.LoginTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20240219145100",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentZettelSxn)},
	id.InfoTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Info HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.FormTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},
	id.RenameTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Rename Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentRenameSxn)},
	id.DeleteTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230704122100",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ErrorTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Error HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20210305133215",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentErrorSxn)},
	id.StartSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Start Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230824160700",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnBase),
		},
		zettel.NewContent(contentStartCodeSxn)},
	id.BaseSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Base Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",
			api.KeyModified:   "20240219144600",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnPrelude),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.PreludeSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Prelude",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20231006181700",
			api.KeyModified:   "20240222121200",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentPreludeSxn)},
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20231129112800",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20210622110143",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent([]byte("/* User-defined CSS */"))},
	id.EmojiZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyCreated:    "20210504175807",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {
		constHeader{
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",
			api.KeyModified:   "20231129111800",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:                 "New Zettel",
			api.KeyRole:                  api.ValueRoleConfiguration,
			api.KeySyntax:                meta.SyntaxZmk,
			api.KeyCreated:               "20201028185209",
			api.KeyModified:              "20230929132900",
			meta.NewPrefix + api.KeyRole: api.ValueRoleZettel,
			api.KeyVisibility:            api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewRole): {
		constHeader{
			api.KeyTitle:                  "New Role",
			api.KeyRole:                   api.ValueRoleConfiguration,
			api.KeySyntax:                 meta.SyntaxZmk,
			api.KeyCreated:                "20231129110800",
			meta.NewPrefix + api.KeyRole:  api.ValueRoleRole,
			meta.NewPrefix + api.KeyTitle: "",
			api.KeyVisibility:             api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewTag): {
		constHeader{
			api.KeyTitle:                  "New Tag",
			api.KeyRole:                   api.ValueRoleConfiguration,
			api.KeySyntax:                 meta.SyntaxZmk,
			api.KeyCreated:                "20230929132400",
			meta.NewPrefix + api.KeyRole:  api.ValueRoleTag,
			meta.NewPrefix + api.KeyTitle: "#",
			api.KeyVisibility:             api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewUser): {
		constHeader{
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.ValueRoleConfiguration,
			api.KeySyntax:                      meta.SyntaxNone,
			api.KeyCreated:                     "20201028185209",
			meta.NewPrefix + api.KeyCredential: "",
			meta.NewPrefix + api.KeyUserID:     "",
			meta.NewPrefix + api.KeyUserRole:   api.ValueUserRoleReader,
			api.KeyVisibility:                  api.ValueVisibilityOwner,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidRoleZettelZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleZettel,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129161400",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleZettel)},
	id.MustParse(api.ZidRoleConfigurationZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleConfiguration,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162800",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleConfiguration)},
	id.MustParse(api.ZidRoleRoleZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleRole,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162900",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleRole)},
	id.MustParse(api.ZidRoleTagZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleTag,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162000",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleTag)},
	id.DefaultHomeZid: {
		constHeader{
			api.KeyTitle:   "Home",
			api.KeyRole:    api.ValueRoleZettel,
			api.KeySyntax:  meta.SyntaxZmk,
			api.KeyLang:    api.ValueLangEN,
			api.KeyCreated: "20210210190757",
		},
		zettel.NewContent(contentHomeZettel)},
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Added box/constbox/contributors.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
Zettelstore is a software for humans made from humans.

=== Licensor(s)
* Detlef Stern [[mailto:ds@zettelstore.de]]
** Main author
** Maintainer

=== Contributors

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

Added box/constbox/dependencies.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Zettelstore is made with the help of other software and other artifacts.
Thank you very much!

This zettel lists all of them, together with their licenses.

=== Go runtime and associated libraries
; License
: BSD 3-Clause "New" or "Revised" License
```
Copyright (c) 2009 The Go Authors. All rights reserved.

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

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

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

=== ASCIIToSVG
; URL
: [[https://github.com/asciitosvg/asciitosvg]]
; License
: MIT
; Remarks
: ASCIIToSVG was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/draw''.
  Later, the source code was changed substantially to adapt it to the needs of Zettelstore.
```
Copyright (c) 2015 The ASCIIToSVG Contributors

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

=== Fsnotify
; URL
: [[https://fsnotify.org/]]
; License
: BSD 3-Clause "New" or "Revised" License
; Source
: [[https://github.com/fsnotify/fsnotify]]
```
Copyright © 2012 The Go Authors. All rights reserved.
Copyright © fsnotify Authors. All rights reserved.

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

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

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

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

Copyright (c) 2019 Yusuke Inuzuka

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

=== sx, zettelstore-client
These are companion projects, written by the current main developer of Zettelstore.
They are published under the same license, [[EUPL v1.2, or later|00000000000004]].

; URL & Source sx
: [[https://zettelstore.de/sx]]
; URL & Source zettelstore-client
: [[https://zettelstore.de/client/]]
; License:
: European Union Public License, version 1.2 (EUPL v1.2), or later.

Added box/constbox/emoji_spin.gif.

cannot compute difference between binary files

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
)

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

Added box/constbox/home.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
=== 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.

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

Additionally, you have to describe, what you have done before that error occurs
and what you have expected instead.
Please do not forget to include the error message, if there is one.

Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"".
Otherwise, only some zettel are linked.
To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]:
please set the metadata value of the key ''expert-mode'' to true.
To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata.

=== Information about this zettel
This zettel is your home zettel.
It is part of the Zettelstore software itself.
Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel.

You can change the content of this zettel by clicking on ""Edit"" above.
This allows you to customize your home zettel.

Alternatively, you can designate another zettel as your home zettel.
Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''.
Its value is the identifier of the zettel that should act as the new home zettel.
You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above.
The identifier of this zettel is ''00010000000000''.
If you provide a wrong identifier, this zettel will be shown as the home zettel.
Take a look inside the manual for further details.

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
48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
;;;----------------------------------------------------------------------------
;;; 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? 'rename-url) `((@H " &#183; ") (a (@ (href ,rename-url)) "Rename")))
      ,@(if (bound? 'delete-url) `((@H " &#183; ") (a (@ (href ,delete-url)) "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
  (h2 "References")
  ,@(if local-links `((h3 "Local")    (ul ,@(map wui-valid-link local-links))))
  ,@(if query-links `((h3 "Queries")  (ul ,@(map wui-item-link query-links))))
  ,@(if ext-links   `((h3 "External") (ul ,@(map wui-item-popup-link ext-links))))
  (h3 "Unlinked")
  ,@unlinked-content
  (form
    (label (@ (for "phrase")) "Search Phrase")
    (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase)))
  )
  (h2 "Parts and encodings")
  ,(wui-enc-matrix enc-eval)
  (h3 "Parsed (not evaluated)")
  ,(wui-enc-matrix enc-parsed)
  ,@(if shadow-links
    `((h2 "Shadowed Boxes")
      (ul ,@(map wui-item shadow-links))
    )
  )
)

Added box/constbox/license.txt.








































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
translations of the other languages.


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


EUROPEAN UNION PUBLIC LICENCE v. 1.2
EUPL © the European Union 2007, 2016

This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
below) which is provided under the terms of this Licence. Any use of the Work,
other than as authorised under this Licence is prohibited (to the extent such
use is covered by a right of the copyright holder of the Work).

The Work is provided under the terms of this Licence when the Licensor (as
defined below) has placed the following notice immediately following the
copyright notice for the Work:

                          Licensed under the EUPL

or has expressed by any other means his willingness to license under the EUPL.

1. Definitions

In this Licence, the following terms have the following meaning:

— ‘The Licence’: this Licence.
— ‘The Original Work’: the work or software distributed or communicated by the
  Licensor under this Licence, available as Source Code and also as Executable
  Code as the case may be.
— ‘Derivative Works’: the works or software that could be created by the
  Licensee, based upon the Original Work or modifications thereof. This Licence
  does not define the extent of modification or dependence on the Original Work
  required in order to classify a work as a Derivative Work; this extent is
  determined by copyright law applicable in the country mentioned in Article
  15.
— ‘The Work’: the Original Work or its Derivative Works.
— ‘The Source Code’: the human-readable form of the Work which is the most
  convenient for people to study and modify.
— ‘The Executable Code’: any code which has generally been compiled and which
  is meant to be interpreted by a computer as a program.
— ‘The Licensor’: the natural or legal person that distributes or communicates
  the Work under the Licence.
— ‘Contributor(s)’: any natural or legal person who modifies the Work under the
  Licence, or otherwise contributes to the creation of a Derivative Work.
— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
  the Work under the terms of the Licence.
— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
  renting, distributing, communicating, transmitting, or otherwise making
  available, online or offline, copies of the Work or providing access to its
  essential functionalities at the disposal of any other natural or legal
  person.

2. Scope of the rights granted by the Licence

The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
sublicensable licence to do the following, for the duration of copyright vested
in the Original Work:

— use the Work in any circumstance and for all usage,
— reproduce the Work,
— modify the Work, and make Derivative Works based upon the Work,
— communicate to the public, including the right to make available or display
  the Work or copies thereof to the public and perform publicly, as the case
  may be, the Work,
— distribute the Work or copies thereof,
— lend and rent the Work or copies thereof,
— sublicense rights in the Work or copies thereof.

Those rights can be exercised on any media, supports and formats, whether now
known or later invented, as far as the applicable law permits so.

In the countries where moral rights apply, the Licensor waives his right to
exercise his moral right to the extent allowed by law in order to make
effective the licence of the economic rights here above listed.

The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
any patents held by the Licensor, to the extent necessary to make use of the
rights granted on the Work under this Licence.

3. Communication of the Source Code

The Licensor may provide the Work either in its Source Code form, or as
Executable Code. If the Work is provided as Executable Code, the Licensor
provides in addition a machine-readable copy of the Source Code of the Work
along with each copy of the Work that the Licensor distributes or indicates, in
a notice following the copyright notice attached to the Work, a repository
where the Source Code is easily and freely accessible for as long as the
Licensor continues to distribute or communicate the Work.

4. Limitations on copyright

Nothing in this Licence is intended to deprive the Licensee of the benefits
from any exception or limitation to the exclusive rights of the rights owners
in the Work, of the exhaustion of those rights or of other applicable
limitations thereto.

5. Obligations of the Licensee

The grant of the rights mentioned above is subject to some restrictions and
obligations imposed on the Licensee. Those obligations are the following:

Attribution right: The Licensee shall keep intact all copyright, patent or
trademarks notices and all notices that refer to the Licence and to the
disclaimer of warranties. The Licensee must include a copy of such notices and
a copy of the Licence with every copy of the Work he/she distributes or
communicates. The Licensee must cause any Derivative Work to carry prominent
notices stating that the Work has been modified and the date of modification.

Copyleft clause: If the Licensee distributes or communicates copies of the
Original Works or Derivative Works, this Distribution or Communication will be
done under the terms of this Licence or of a later version of this Licence
unless the Original Work is expressly distributed only under this version of
the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
(becoming Licensor) cannot offer or impose any additional terms or conditions
on the Work or Derivative Work that alter or restrict the terms of the Licence.

Compatibility clause: If the Licensee Distributes or Communicates Derivative
Works or copies thereof based upon both the Work and another work licensed
under a Compatible Licence, this Distribution or Communication can be done
under the terms of this Compatible Licence. For the sake of this clause,
‘Compatible Licence’ refers to the licences listed in the appendix attached to
this Licence. Should the Licensee's obligations under the Compatible Licence
conflict with his/her obligations under this Licence, the obligations of the
Compatible Licence shall prevail.

Provision of Source Code: When distributing or communicating copies of the
Work, the Licensee will provide a machine-readable copy of the Source Code or
indicate a repository where this Source will be easily and freely available for
as long as the Licensee continues to distribute or communicate the Work.

Legal Protection: This Licence does not grant permission to use the trade
names, trademarks, service marks, or names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the copyright notice.

6. Chain of Authorship

The original Licensor warrants that the copyright in the Original Work granted
hereunder is owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.

Each Contributor warrants that the copyright in the modifications he/she brings
to the Work are owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.

Each time You accept the Licence, the original Licensor and subsequent
Contributors grant You a licence to their contributions to the Work, under the
terms of this Licence.

7. Disclaimer of Warranty

The Work is a work in progress, which is continuously improved by numerous
Contributors. It is not a finished work and may therefore contain defects or
‘bugs’ inherent to this type of development.

For the above reason, the Work is provided under the Licence on an ‘as is’
basis and without warranties of any kind concerning the Work, including without
limitation merchantability, fitness for a particular purpose, absence of
defects or errors, accuracy, non-infringement of intellectual property rights
other than copyright as stated in Article 6 of this Licence.

This disclaimer of warranty is an essential part of the Licence and a condition
for the grant of any rights to the Work.

8. Disclaimer of Liability

Except in the cases of wilful misconduct or damages directly caused to natural
persons, the Licensor will in no event be liable for any direct or indirect,
material or moral, damages of any kind, arising out of the Licence or of the
use of the Work, including without limitation, damages for loss of goodwill,
work stoppage, computer failure or malfunction, loss of data or any commercial
damage, even if the Licensor has been advised of the possibility of such
damage. However, the Licensor will be liable under statutory product liability
laws as far such laws apply to the Work.

9. Additional agreements

While distributing the Work, You may choose to conclude an additional
agreement, defining obligations or services consistent with this Licence.
However, if accepting obligations, You may act only on your own behalf and on
your sole responsibility, not on behalf of the original Licensor or any other
Contributor, and only if You agree to indemnify, defend, and hold each
Contributor harmless for any liability incurred by, or claims asserted against
such Contributor by the fact You have accepted any warranty or additional
liability.

10. Acceptance of the Licence

The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
placed under the bottom of a window displaying the text of this Licence or by
affirming consent in any other similar way, in accordance with the rules of
applicable law. Clicking on that icon indicates your clear and irrevocable
acceptance of this Licence and all of its terms and conditions.

Similarly, you irrevocably accept this Licence and all of its terms and
conditions by exercising any rights granted to You by Article 2 of this
Licence, such as the use of the Work, the creation by You of a Derivative Work
or the Distribution or Communication by You of the Work or copies thereof.

11. Information to the public

In case of any Distribution or Communication of the Work by means of electronic
communication by You (for example, by offering to download the Work from
a remote location) the distribution channel or media (for example, a website)
must at least provide to the public the information requested by the applicable
law regarding the Licensor, the Licence and the way it may be accessible,
concluded, stored and reproduced by the Licensee.

12. Termination of the Licence

The Licence and the rights granted hereunder will terminate automatically upon
any breach by the Licensee of the terms of the Licence.

Such a termination will not terminate the licences of any person who has
received the Work from the Licensee under the Licence, provided such persons
remain in full compliance with the Licence.

13. Miscellaneous

Without prejudice of Article 9 above, the Licence represents the complete
agreement between the Parties as to the Work.

If any provision of the Licence is invalid or unenforceable under applicable
law, this will not affect the validity or enforceability of the Licence as
a whole. Such provision will be construed or reformed so as necessary to make
it valid and enforceable.

The European Commission may publish other linguistic versions or new versions
of this Licence or updated versions of the Appendix, so far this is required
and reasonable, without reducing the scope of the rights granted by the
Licence. New versions of the Licence will be published with a unique version
number.

All linguistic versions of this Licence, approved by the European Commission,
have identical value. Parties can take advantage of the linguistic version of
their choice.

14. Jurisdiction

Without prejudice to specific agreement between parties,

— any litigation resulting from the interpretation of this License, arising
  between the European Union institutions, bodies, offices or agencies, as
  a Licensor, and any Licensee, will be subject to the jurisdiction of the
  Court of Justice of the European Union, as laid down in article 272 of the
  Treaty on the Functioning of the European Union,
— any litigation arising between other parties and resulting from the
  interpretation of this License, will be subject to the exclusive jurisdiction
  of the competent court where the Licensor resides or conducts its primary
  business.

15. Applicable Law

Without prejudice to specific agreement between parties,

— this Licence shall be governed by the law of the European Union Member State
  where the Licensor has his seat, resides or has his registered office,
— this licence shall be governed by Belgian law if the Licensor has no seat,
  residence or registered office inside a European Union Member State.


                                  Appendix


‘Compatible Licences’ according to Article 5 EUPL are:

— GNU General Public License (GPL) v. 2, v. 3
— GNU Affero General Public License (AGPL) v. 3
— Open Software License (OSL) v. 2.1, v. 3.0
— Eclipse Public License (EPL) v. 1.0
— CeCILL v. 2.0, v. 2.1
— Mozilla Public Licence (MPL) v. 2
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
  works other than software
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
  Reciprocity (LiLiQ-R+)

The European Commission may update this Appendix to later versions of the above
licences without producing a new version of the EUPL, as long as they provide
the rights granted in Article 2 of this Licence and protect the covered Source
Code from exclusive appropriation.

All other changes or additions to this Appendix require the production of a new
EUPL version.

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

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

Added box/constbox/newtoc.zettel.







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

Added box/constbox/rename.sxn.











































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Rename Zettel " ,zid))
  (p "Do you really want to rename this zettel?")
  ,@(if incoming
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "If you rename this zettel, incoming references from the following zettel will become invalid.")
      (ul ,@(map wui-item-link incoming))
    ))
  )
  ,@(if (and (bound? 'useless) useless)
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "Renaming this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.")
      (ul ,@(map wui-item useless))
    ))
  )
  (form (@ (method "POST"))
    (input (@ (type "hidden") (id "curzid") (name "curzid") (value ,zid)))
    (div
      (label (@ (for "newzid")) "New zettel id")
      (input (@ (class "zs-input") (type "text") (inputmode "numeric") (id "newzid") (name "newzid")
                (pattern "\\d{14}")
                (title "New zettel identifier, must be unique")
                (placeholder "ZID..") (value ,zid) (autofocus))))
    (div (input (@ (class "zs-primary") (type "submit") (value "Rename"))))
  )
  ,(wui-meta-desc metapairs)
)

Added box/constbox/roleconfiguration.zettel.





















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

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

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

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

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

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

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

Added box/constbox/rolerole.zettel.











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

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

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

Added box/constbox/roletag.zettel.







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

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

Added box/constbox/rolezettel.zettel.








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

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

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

Added box/constbox/start.sxn.


















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

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

Added box/constbox/wuicode.sxn.
















































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

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

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

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

;; wui-valid-link translates a local link into a HTML link. A link is a pair
;; (valid . url). If valid is not truish, only the invalid url is returned.
(defun wui-valid-link (l)
    (if (car l)
        `(li (a (@ (href ,(cdr l))) ,(cdr l)))
        `(li ,(cdr l))))

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

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

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

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

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

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

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

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

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

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

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

;; ACTION-SEPARATOR defines a HTML value that separates actions links.
(defvar ACTION-SEPARATOR '(@H " &#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 ((child-url (binding-lookup 'child-url binding)))
             (if (defined? child-url) `((@H " &#183; ") (a (@ (href ,child-url)) "Child"))))
      ,@(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)))
    )
)

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 superior-refs `((br) "Superior: " ,superior-refs))
      ,@(ROLE-DEFAULT-heading (current-binding))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links subordinate-links back-links successor-links)
    `((nav
      ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
      ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links)))))
      ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
      ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links)))))
     ))
  )
)

Added 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

import (
	"context"
	"errors"
	"net/url"
	"os"
	"path/filepath"
	"sync"

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

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		var log *logger.Logger
		if krnl := kernel.Main; krnl != nil {
			log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child()
		}
		path := getDirPath(u)
		if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
			return nil, err
		}
		dp := dirBox{
			log:        log,
			number:     cdata.Number,
			location:   u.String(),
			readonly:   box.GetQueryBool(u, "readonly"),
			cdata:      *cdata,
			dir:        path,
			notifySpec: getDirSrvInfo(log, u.Query().Get("type")),
			fSrvs:      makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))),
		}
		return &dp, nil
	})
}

func makePrime(n uint32) uint32 {
	for !isPrime(n) {
		n++
	}
	return n
}

func isPrime(n uint32) bool {
	if n == 0 {
		return false
	}
	if n <= 3 {
		return true
	}
	if n%2 == 0 {
		return false
	}
	for i := uint32(3); i*i <= n; i += 2 {
		if n%i == 0 {
			return false
		}
	}
	return true
}

type notifyTypeSpec int

const (
	_ notifyTypeSpec = iota
	dirNotifyAny
	dirNotifySimple
	dirNotifyFS
)

func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec {
	for range 2 {
		switch notifyType {
		case kernel.BoxDirTypeNotify:
			return dirNotifyFS
		case kernel.BoxDirTypeSimple:
			return dirNotifySimple
		default:
			notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string)
		}
	}
	log.Error().Str("notifyType", notifyType).Msg("Unable to set notify type, using a default")
	return dirNotifySimple
}

func getDirPath(u *url.URL) string {
	if u.Opaque != "" {
		return filepath.Clean(u.Opaque)
	}
	return filepath.Clean(u.Path)
}

// dirBox uses a directory to store zettel as files.
type dirBox struct {
	log        *logger.Logger
	number     int
	location   string
	readonly   bool
	cdata      manager.ConnectData
	dir        string
	notifySpec notifyTypeSpec
	dirSrv     *notify.DirService
	fSrvs      uint32
	fCmds      []chan fileCmd
	mxCmds     sync.RWMutex
}

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

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

func (dp *dirBox) Start(context.Context) error {
	dp.mxCmds.Lock()
	defer dp.mxCmds.Unlock()
	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
	for i := range dp.fSrvs {
		cc := make(chan fileCmd)
		go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc)
		dp.fCmds = append(dp.fCmds, cc)
	}

	var notifier notify.Notifier
	var err error
	switch dp.notifySpec {
	case dirNotifySimple:
		notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir)
	default:
		notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir)
	}
	if err != nil {
		dp.log.Error().Err(err).Msg("Unable to create directory supervisor")
		dp.stopFileServices()
		return err
	}
	dp.dirSrv = notify.NewDirService(
		dp,
		dp.log.Clone().Str("sub", "dirsrv").Child(),
		notifier,
		dp.cdata.Notify,
	)
	dp.dirSrv.Start()
	return nil
}

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

func (dp *dirBox) Stop(_ context.Context) {
	dirSrv := dp.dirSrv
	dp.dirSrv = nil
	if dirSrv != nil {
		dirSrv.Stop()
	}
	dp.stopFileServices()
}

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

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

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

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

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

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

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

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

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

func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool {
	return dp.dirSrv.GetDirEntry(zid).IsValid()
}

func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	entries := dp.dirSrv.GetDirEntries(constraint)
	dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid")
	for _, entry := range entries {
		handle(entry.Zid)
	}
	return nil
}

func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	entries := dp.dirSrv.GetDirEntries(constraint)
	dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta")

	// The following loop could be parallelized if needed for performance.
	for _, entry := range entries {
		m, err := dp.srvGetMeta(ctx, entry, entry.Zid)
		if err != nil {
			dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta")
			return err
		}
		dp.cdata.Enricher.Enrich(ctx, m, dp.number)
		handle(m)
	}
	return nil
}

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

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

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

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

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

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

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

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

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

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

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

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

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

Added box/dirbox/dirbox_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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) {
	testcases := []struct {
		n   uint32
		exp bool
	}{
		{0, false}, {1, true}, {2, true}, {3, true}, {4, false}, {5, true},
		{6, false}, {7, true}, {8, false}, {9, false}, {10, false},
		{11, true}, {12, false}, {13, true}, {14, false}, {15, false},
		{17, true}, {19, true}, {21, false}, {23, true}, {25, false},
		{27, false}, {29, true}, {31, true}, {33, false}, {35, false},
	}
	for _, tc := range testcases {
		got := isPrime(tc.n)
		if got != tc.exp {
			t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got)
		}
	}
}

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

Added 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
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/box/filebox"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

type fileCmd interface {
	run(string)
}

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

// COMMAND: srvGetMeta ----------------------------------------
//
// Retrieves the meta data from a zettel.

func (dp *dirBox) srvGetMeta(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, error) {
	rc := make(chan resGetMeta, 1)
	dp.getFileChan(zid) <- &fileGetMeta{entry, rc}
	ctx, cancel := context.WithTimeout(ctx, serviceTimeout)
	defer cancel()
	select {
	case res := <-rc:
		return res.meta, res.err
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

type fileGetMeta struct {
	entry *notify.DirEntry
	rc    chan<- resGetMeta
}
type resGetMeta struct {
	meta *meta.Meta
	err  error
}

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

	entry := cmd.entry
	zid := entry.Zid
	if metaName := entry.MetaName; metaName == "" {
		contentName := entry.ContentName
		contentExt := entry.ContentExt
		if contentName == "" || contentExt == "" {
			err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid)
		} else if entry.HasMetaInContent() {
			m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName))
		} else {
			m = filebox.CalcDefaultMeta(zid, contentExt)
		}
	} else {
		m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName))
	}
	if err == nil {
		cmdCleanupMeta(m, entry)
	}
	cmd.rc <- resGetMeta{m, err}
}

// COMMAND: srvGetMetaContent ----------------------------------------
//
// Retrieves the meta data and the content of a zettel.

func (dp *dirBox) srvGetMetaContent(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, []byte, error) {
	rc := make(chan resGetMetaContent, 1)
	dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc}
	ctx, cancel := context.WithTimeout(ctx, serviceTimeout)
	defer cancel()
	select {
	case res := <-rc:
		return res.meta, res.content, res.err
	case <-ctx.Done():
		return nil, nil, ctx.Err()
	}
}

type fileGetMetaContent struct {
	entry *notify.DirEntry
	rc    chan<- resGetMetaContent
}
type resGetMetaContent struct {
	meta    *meta.Meta
	content []byte
	err     error
}

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

	entry := cmd.entry
	zid := entry.Zid
	contentName := entry.ContentName
	contentExt := entry.ContentExt
	contentPath := filepath.Join(dirPath, contentName)
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" || contentExt == "" {
			err = fmt.Errorf("no meta, no content in getMetaContent, zid=%v", zid)
		} else if entry.HasMetaInContent() {
			m, content, err = parseMetaContentFile(zid, contentPath)
		} else {
			m = filebox.CalcDefaultMeta(zid, contentExt)
			content, err = os.ReadFile(contentPath)
		}
	} else {
		m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName))
		if contentName != "" {
			var err1 error
			content, err1 = os.ReadFile(contentPath)
			if err == nil {
				err = err1
			}
		}
	}
	if err == nil {
		cmdCleanupMeta(m, entry)
	}
	cmd.rc <- resGetMetaContent{m, content, err}
}

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

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

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

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

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

func writeMetaFile(metaPath string, m *meta.Meta) error {
	metaFile, err := openFileWrite(metaPath)
	if err != nil {
		return err
	}
	err = writeFileZid(metaFile, m.Zid)
	if err == nil {
		_, err = m.WriteComputed(metaFile)
	}
	if err1 := metaFile.Close(); err == nil {
		err = err1
	}
	return err
}

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

var (
	newline = []byte{'\n'}
	yamlSep = []byte{'-', '-', '-', '\n'}
)

func writeMetaHeader(w io.Writer, m *meta.Meta) (err error) {
	if m.YamlSep {
		_, err = w.Write(yamlSep)
		if err != nil {
			return err
		}
	}
	err = writeFileZid(w, m.Zid)
	if err != nil {
		return err
	}
	_, err = m.WriteComputed(w)
	if err != nil {
		return err
	}
	if m.YamlSep {
		_, err = w.Write(yamlSep)
	} else {
		_, err = w.Write(newline)
	}
	return err
}

// COMMAND: srvDeleteZettel ----------------------------------------
//
// Deletes an existing zettel.

func (dp *dirBox) srvDeleteZettel(ctx context.Context, entry *notify.DirEntry, zid id.Zid) error {
	rc := make(chan resDeleteZettel, 1)
	dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc}
	ctx, cancel := context.WithTimeout(ctx, serviceTimeout)
	defer cancel()
	select {
	case err := <-rc:
		return err
	case <-ctx.Done():
		return ctx.Err()
	}
}

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

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

	entry := cmd.entry
	contentName := entry.ContentName
	contentPath := filepath.Join(dirPath, contentName)
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" {
			err = fmt.Errorf("no meta, no content in deleteZettel, zid=%v", entry.Zid)
		} else {
			err = os.Remove(contentPath)
		}
	} else {
		if contentName != "" {
			err = os.Remove(contentPath)
		}
		err1 := os.Remove(filepath.Join(dirPath, metaName))
		if err == nil {
			err = err1
		}
	}
	for _, dupName := range entry.UselessFiles {
		err1 := os.Remove(filepath.Join(dirPath, dupName))
		if err == nil {
			err = err1
		}
	}
	cmd.rc <- err
}

// Utility functions ----------------------------------------

func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) {
	src, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	inp := input.NewInput(src)
	return meta.NewFromInput(zid, inp), nil
}

func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, []byte, error) {
	src, err := os.ReadFile(path)
	if err != nil {
		return nil, nil, err
	}
	inp := input.NewInput(src)
	meta := meta.NewFromInput(zid, inp)
	return meta, src[inp.Pos:], nil
}

func cmdCleanupMeta(m *meta.Meta, entry *notify.DirEntry) {
	filebox.CleanupMeta(
		m,
		entry.Zid,
		entry.ContentExt,
		entry.MetaName != "",
		entry.UselessFiles,
	)
}

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

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

func writeFileContent(path string, content []byte) error {
	f, err := openFileWrite(path)
	if err == nil {
		_, err = f.Write(content)
		if err1 := f.Close(); err == nil {
			err = err1
		}
	}
	return err
}

Added 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 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/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		path := getFilepathFromURL(u)
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".zip" {
			return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String())
		}
		return &zipBox{
			log: kernel.Main.GetLogger(kernel.BoxService).Clone().
				Str("box", "zip").Int("boxnum", int64(cdata.Number)).Child(),
			number:   cdata.Number,
			name:     path,
			enricher: cdata.Enricher,
			notify:   cdata.Notify,
		}, nil
	})
}

func getFilepathFromURL(u *url.URL) string {
	name := u.Opaque
	if name == "" {
		name = u.Path
	}
	components := strings.Split(name, "/")
	fileName := filepath.Join(components...)
	if len(components) > 0 && components[0] == "" {
		return "/" + fileName
	}
	return fileName
}

var alternativeSyntax = map[string]string{
	"htm": "html",
}

func calculateSyntax(ext string) string {
	ext = strings.ToLower(ext)
	if syntax, ok := alternativeSyntax[ext]; ok {
		return syntax
	}
	return ext
}

// CalcDefaultMeta returns metadata with default values for the given entry.
func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta {
	m := meta.New(zid)
	m.Set(api.KeySyntax, calculateSyntax(ext))
	return m
}

// CleanupMeta enhances the given metadata.
func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) {
	if inMeta {
		if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
			dm := CalcDefaultMeta(zid, ext)
			syntax, ok = dm.Get(api.KeySyntax)
			if !ok {
				panic("Default meta must contain syntax")
			}
			m.Set(api.KeySyntax, syntax)
		}
	}

	if len(uselessFiles) > 0 {
		m.Set(api.KeyUselessFiles, strings.Join(uselessFiles, " "))
	}
}

Added box/filebox/zipbox.go.

























































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

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

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

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

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

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

	contentName := entry.ContentName
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" {
			err = fmt.Errorf("no meta, no content in getZettel, zid=%v", zid)
			return zettel.Zettel{}, err
		}
		src, err = readZipFileContent(reader, entry.ContentName)
		if err != nil {
			return zettel.Zettel{}, err
		}
		if entry.HasMetaInContent() {
			inp := input.NewInput(src)
			m = meta.NewFromInput(zid, inp)
			src = src[inp.Pos:]
		} else {
			m = CalcDefaultMeta(zid, entry.ContentExt)
		}
	} else {
		m, err = readZipMetaFile(reader, zid, metaName)
		if err != nil {
			return zettel.Zettel{}, err
		}
		inMeta = true
		if contentName != "" {
			src, err = readZipFileContent(reader, entry.ContentName)
			if err != nil {
				return zettel.Zettel{}, err
			}
		}
	}

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

func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool {
	return zb.dirSrv.GetDirEntry(zid).IsValid()
}

func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	entries := zb.dirSrv.GetDirEntries(constraint)
	zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid")
	for _, entry := range entries {
		handle(entry.Zid)
	}
	return nil
}

func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return err
	}
	defer reader.Close()
	entries := zb.dirSrv.GetDirEntries(constraint)
	zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta")
	for _, entry := range entries {
		if !constraint(entry.Zid) {
			continue
		}
		m, err2 := zb.readZipMeta(reader, entry.Zid, entry)
		if err2 != nil {
			continue
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}

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

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

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

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

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

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

func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) {
	src, err := readZipFileContent(reader, name)
	if err != nil {
		return nil, err
	}
	inp := input.NewInput(src)
	return meta.NewFromInput(zid, inp), nil
}

func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) {
	f, err := reader.Open(name)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return io.ReadAll(f)
}

Added 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package box

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

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

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

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

// GetQueryInt is a helper function to extract int values of a specified range from a box URI.
func GetQueryInt(u *url.URL, key string, 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
}

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

package manager

import (
	"sync"

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

type arAction int

const (
	arNothing arAction = iota
	arReload
	arZettel
)

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

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

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

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

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

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

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

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

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

func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	first := ar.first
	if first != nil {
		if first.waiting == nil && first.reload {
			ar.removeFirst()
			return arReload, id.Invalid, false
		}
		for zid := range first.waiting {
			delete(first.waiting, zid)
			if len(first.waiting) == 0 {
				ar.removeFirst()
			}
			return arZettel, zid, first.reload
		}
		ar.removeFirst()
	}
	return arNothing, id.Invalid, false
}

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

Added 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

import (
	"testing"

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

func TestSimple(t *testing.T) {
	t.Parallel()
	ar := newAnteroomQueue(2)
	ar.EnqueueZettel(id.Zid(1))
	action, zid, lastReload := ar.Dequeue()
	if zid != id.Zid(1) || action != arZettel || lastReload {
		t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload)
	}
	_, zid, _ = ar.Dequeue()
	if zid != id.Invalid {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.EnqueueZettel(id.Zid(1))
	ar.EnqueueZettel(id.Zid(2))
	if ar.first != ar.last {
		t.Errorf("Expected one room, but got more")
	}
	ar.EnqueueZettel(id.Zid(3))
	if ar.first == ar.last {
		t.Errorf("Expected more than one room, but got only one")
	}

	count := 0
	for ; count < 1000; count++ {
		action, _, _ = ar.Dequeue()
		if action == arNothing {
			break
		}
	}
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	t.Parallel()
	ar := newAnteroomQueue(1)
	ar.EnqueueZettel(id.Zid(1))
	ar.Reset()
	action, zid, _ := ar.Dequeue()
	if action != arReload || zid != id.Invalid {
		t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
	}
	ar.Reload(id.NewSet(3, 4))
	ar.EnqueueZettel(id.Zid(5))
	ar.EnqueueZettel(id.Zid(5))
	if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ {
		t.Errorf("Expected 2 rooms")
	}
	action, zid1, _ := ar.Dequeue()
	if action != arZettel {
		t.Errorf("Expected arZettel, but got %v", action)
	}
	action, zid2, _ := ar.Dequeue()
	if action != arZettel {
		t.Errorf("Expected arZettel, but got %v", action)
	}
	if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) {
		t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2)
	}
	action, zid, _ = ar.Dequeue()
	if zid != id.Zid(5) || action != arZettel {
		t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action)
	}
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

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

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

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

package manager

import (
	"context"
	"errors"
	"strings"

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

// Conatains all box.Box related functions

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

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

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

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel")
	if mgr.State() != box.StartStateStarted {
		return zettel.Zettel{}, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for i, p := range mgr.boxes {
		var errZNF box.ErrZettelNotFound
		if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) {
			if err == nil {
				mgr.Enrich(ctx, z.Meta, i+1)
			}
			return z, err
		}
	}
	return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
}

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

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZids")
	if mgr.State() != box.StartStateStarted {
		return nil, box.ErrStopped
	}
	result := id.Set{}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, func(id.Zid) bool { return true })
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

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

func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta")
	if mgr.State() != box.StartStateStarted {
		return nil, box.ErrStopped
	}

	m, err := mgr.idxStore.GetMeta(ctx, zid)
	if err != nil {
		return nil, err
	}
	mgr.Enrich(ctx, m, 0)
	return m, nil
}

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

	compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq)
	if result := compSearch.Result(); result != nil {
		mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta")
		return result, nil
	}
	selected := map[id.Zid]*meta.Meta{}
	for _, term := range compSearch.Terms {
		rejected := id.Set{}
		handleMeta := func(m *meta.Meta) {
			zid := m.Zid
			if rejected.ContainsOrNil(zid) {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected")
				return
			}
			if _, ok := selected[zid]; ok {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected")
				return
			}
			if compSearch.PreMatch(m) && term.Match(m) {
				selected[zid] = m
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match")
			} else {
				rejected.Add(zid)
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject")
			}
		}
		for _, p := range mgr.boxes {
			if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil {
				return nil, err2
			}
		}
	}
	result := make([]*meta.Meta, 0, len(selected))
	for _, m := range selected {
		result = append(result, m)
	}
	result = compSearch.AfterSearch(result)
	mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta")
	return result, nil
}

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

}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox {
		zettel.Meta = mgr.cleanMetaProperties(zettel.Meta)
		if err := box.UpdateZettel(ctx, zettel); err != nil {
			return err
		}
		mgr.idxUpdateZettel(ctx, zettel)
		return nil
	}
	return box.ErrReadOnly
}

// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	if mgr.State() != box.StartStateStarted {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if !p.AllowRenameZettel(ctx, zid) {
			return false
		}
	}
	return true
}

// RenameZettel changes the current zid to a new zid.
func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for i, p := range mgr.boxes {
		err := p.RenameZettel(ctx, curZid, newZid)
		var errZNF box.ErrZettelNotFound
		if err != nil && !errors.As(err, &errZNF) {
			for j := range i {
				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}
	mgr.idxRenameZettel(ctx, curZid, newZid)
	return nil
}

// CanDeleteZettel returns true, if box could possibly delete the given zettel.
func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	if mgr.State() != box.StartStateStarted {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

// DeleteZettel removes the zettel from the box.
func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zid)
		if err == nil {
			mgr.idxDeleteZettel(ctx, zid)
			return nil
		}
		var errZNF box.ErrZettelNotFound
		if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrZettelNotFound{Zid: zid}
}

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

Added box/manager/collect.go.





















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

import (
	"strings"

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

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

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

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

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

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

func (data *collectData) addText(s string) {
	for _, word := range strfun.NormalizeWords(s) {
		data.words.Add(word)
	}
}

func (data *collectData) addRef(ref *ast.Reference) {
	if ref == nil {
		return
	}
	if ref.IsExternal() {
		data.urls.Add(strings.ToLower(ref.Value))
	}
	if !ref.IsZettel() {
		return
	}
	if zid, err := id.Parse(ref.URL.Path); err == nil {
		data.refs.Add(zid)
	}
}

Added 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
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

	if box.DoNotEnrich(ctx) {
		// Enrich is called indirectly via indexer or enrichment is not requested
		// because of other reasons -> ignore this call, do not update metadata
		return
	}
	computePublished(m)
	if boxNumber > 0 {
		m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber))
	}
	mgr.idxStore.Enrich(ctx, m)
}

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

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

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

func computePublished(m *meta.Meta) {
	if _, ok := m.Get(api.KeyPublished); ok {
		return
	}
	if modified, ok := m.Get(api.KeyModified); ok {
		if _, ok = meta.TimeValue(modified); ok {
			m.Set(api.KeyPublished, modified)
			return
		}
	}
	if created, ok := m.Get(api.KeyCreated); ok {
		if _, ok = meta.TimeValue(created); ok {
			m.Set(api.KeyPublished, created)
			return
		}
	}
	zid := m.Zid.String()
	if _, ok := meta.TimeValue(zid); ok {
		m.Set(api.KeyPublished, zid)
		return
	}

	// Neither the zettel was modified nor the zettel identifer contains a valid
	// timestamp. In this case do not set the "published" property.
}

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

package manager

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

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

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

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

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

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

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

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := box.NoEnrichContext(context.Background())
	for {
		mgr.idxWorkService(ctx)
		if !mgr.idxSleepService(timer, timerDuration) {
			return
		}
	}
}

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

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

func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) {
	var cData collectData
	cData.initialize()
	collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData)

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

func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
	for _, pair := range m.ComputedPairs() {
		descr := meta.GetDescription(pair.Key)
		if descr.IsProperty() {
			continue
		}
		switch descr.Type {
		case meta.TypeID:
			mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi)
		case meta.TypeIDSet:
			for _, val := range meta.ListFromValue(pair.Value) {
				mgr.idxUpdateValue(ctx, descr.Inverse, val, zi)
			}
		case meta.TypeZettelmarkup:
			is := parser.ParseMetadata(pair.Value)
			collectInlineIndexData(&is, cData)
		case meta.TypeURL:
			if _, err := url.Parse(pair.Value); err == nil {
				cData.urls.Add(pair.Value)
			}
		default:
			if descr.Type.IsSet {
				for _, val := range meta.ListFromValue(pair.Value) {
					idxCollectMetaValue(cData.words, val)
				}
			} else {
				idxCollectMetaValue(cData.words, pair.Value)
			}
		}
	}
}

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

func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
	for ref := range cData.refs {
		if mgr.HasZettel(ctx, ref) {
			zi.AddBackRef(ref)
		} else {
			zi.AddDeadRef(ref)
		}
	}
	zi.SetWords(cData.words)
	zi.SetUrls(cData.urls)
}

func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if !mgr.HasZettel(ctx, zid) {
		zi.AddDeadRef(zid)
		return
	}
	if inverseKey == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddInverseRef(inverseKey, zid)
}

func (mgr *Manager) idxRenameZettel(ctx context.Context, curZid, newZid id.Zid) {
	toCheck := mgr.idxStore.RenameZettel(ctx, curZid, newZid)
	mgr.idxCheckZettel(toCheck)
}

func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) {
	toCheck := mgr.idxStore.DeleteZettel(ctx, zid)
	mgr.idxCheckZettel(toCheck)
}

func (mgr *Manager) idxCheckZettel(s id.Set) {
	for zid := range s {
		mgr.idxAr.EnqueueZettel(zid)
	}
}

Added 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
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

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

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

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/mapstore"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

// Connect returns a handle to the specified box.
func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) {
	if authManager.IsReadonly() {
		rawURL := u.String()
		// TODO: the following is wrong under some circumstances:
		// 1. fragment is set
		if q := u.Query(); len(q) == 0 {
			rawURL += "?readonly"
		} else if _, ok := q["readonly"]; !ok {
			rawURL += "&readonly"
		}
		var err error
		if u, err = url.Parse(rawURL); err != nil {
			return nil, err
		}
	}

	if create, ok := registry[u.Scheme]; ok {
		return create(u, cdata)
	}
	return nil, &ErrInvalidScheme{u.Scheme}
}

// ErrInvalidScheme is returned if there is no box with the given scheme.
type ErrInvalidScheme struct{ Scheme string }

func (err *ErrInvalidScheme) Error() string { return "Invalid scheme: " + err.Scheme }

type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error)

var registry = map[string]createFunc{}

// Register the encoder for later retrieval.
func Register(scheme string, create createFunc) {
	if _, ok := registry[scheme]; ok {
		panic(scheme)
	}
	registry[scheme] = create
}

// Manager is a coordinating box.
type Manager struct {
	mgrLog       *logger.Logger
	stateMx      sync.RWMutex
	state        box.StartState
	mgrMx        sync.RWMutex
	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys strfun.Set // Set of property key names

	// Indexer data
	idxLog   *logger.Logger
	idxStore store.Store
	idxAr    *anteroomQueue
	idxReady chan struct{} // Signal a non-empty anteroom to background task

	// Indexer stats data
	idxMx          sync.RWMutex
	idxLastReload  time.Time
	idxDurReload   time.Duration
	idxSinceReload uint64
}

func (mgr *Manager) setState(newState box.StartState) {
	mgr.stateMx.Lock()
	mgr.state = newState
	mgr.stateMx.Unlock()
}

func (mgr *Manager) State() box.StartState {
	mgr.stateMx.RLock()
	state := mgr.state
	mgr.stateMx.RUnlock()
	return state
}

// New creates a new managing box.
func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) {
	descrs := meta.GetSortedKeyDescriptions()
	propertyKeys := make(strfun.Set, len(descrs))
	for _, kd := range descrs {
		if kd.IsProperty() {
			propertyKeys.Set(kd.Name)
		}
	}
	boxLog := kernel.Main.GetLogger(kernel.BoxService)
	mgr := &Manager{
		mgrLog:       boxLog.Clone().Str("box", "manager").Child(),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

		idxLog:   boxLog.Clone().Str("box", "index").Child(),
		idxStore: createIdxStore(rtConfig),
		idxAr:    newAnteroomQueue(1000),
		idxReady: make(chan struct{}, 1),
	}
	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
			return nil, err
		}
		if p != nil {
			boxes = append(boxes, p)
			cdata.Number++
		}
	}
	constbox, err := registry[" const"](nil, &cdata)
	if err != nil {
		return nil, err
	}
	cdata.Number++
	compbox, err := registry[" comp"](nil, &cdata)
	if err != nil {
		return nil, err
	}
	cdata.Number++
	boxes = append(boxes, constbox, compbox)
	mgr.boxes = boxes
	return mgr, nil
}

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

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

func (mgr *Manager) notifier() {
	// The call to notify may panic. Ensure a running notifier.
	defer func() {
		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("Notifier", ri)
			go mgr.notifier()
		}
	}()

	tsLastEvent := time.Now()
	cache := destutterCache{}
	for {
		select {
		case ci, ok := <-mgr.infos:
			if ok {
				now := time.Now()
				if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) {
					// Cache contains entries and is definitely outdated
					mgr.mgrLog.Trace().Msg("clean destutter cache")
					cache = destutterCache{}
				}
				tsLastEvent = now

				reason, zid := ci.Reason, ci.Zid
				mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier")
				if ignoreUpdate(cache, now, reason, zid) {
					mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored")
					continue
				}

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

type destutterData struct {
	deadAt time.Time
	reason box.UpdateReason
}
type destutterCache = map[id.Zid]destutterData

func ignoreUpdate(cache destutterCache, now time.Time, reason box.UpdateReason, zid id.Zid) bool {
	if dsd, found := cache[zid]; found {
		if dsd.reason == reason && dsd.deadAt.After(now) {
			return true
		}
	}
	cache[zid] = destutterData{
		deadAt: now.Add(500 * time.Millisecond),
		reason: reason,
	}
	return false
}

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
	switch reason {
	case box.OnReady:
		return
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}

func (mgr *Manager) notifyObserver(ci *box.UpdateInfo) {
	mgr.mxObserver.RLock()
	observers := mgr.observers
	mgr.mxObserver.RUnlock()
	for _, ob := range observers {
		ob(*ci)
	}
}

// Start the box. Now all other functions of the box are allowed.
// Starting an already started box is not allowed.
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if mgr.State() != box.StartStateStopped {
		return box.ErrStarted
	}
	mgr.setState(box.StartStateStarting)
	for i := len(mgr.boxes) - 1; i >= 0; i-- {
		ssi, ok := mgr.boxes[i].(box.StartStopper)
		if !ok {
			continue
		}
		err := ssi.Start(ctx)
		if err == nil {
			continue
		}
		mgr.setState(box.StartStateStopping)
		for j := i + 1; j < len(mgr.boxes); j++ {
			if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 {
				ssj.Stop(ctx)
			}
		}
		mgr.setState(box.StartStateStopped)
		return err
	}
	mgr.idxAr.Reset() // Ensure an initial index run
	mgr.done = make(chan struct{})
	go mgr.notifier()

	mgr.waitBoxesAreStarted()
	mgr.setState(box.StartStateStarted)
	mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady})

	go mgr.idxIndexer()
	return nil
}

func (mgr *Manager) waitBoxesAreStarted() {
	const waitTime = 10 * time.Millisecond
	const waitLoop = int(1 * time.Second / waitTime)
	for i := 1; !mgr.allBoxesStarted(); i++ {
		if i%waitLoop == 0 {
			if time.Duration(i)*waitTime > time.Minute {
				mgr.mgrLog.Info().Msg("Waiting for more than one minute to start")
			} else {
				mgr.mgrLog.Trace().Msg("Wait for boxes to start")
			}
		}
		time.Sleep(waitTime)
	}
}

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

// Stop the started box. Now only the Start() function is allowed.
func (mgr *Manager) Stop(ctx context.Context) {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if mgr.State() != box.StartStateStarted {
		return
	}
	mgr.setState(box.StartStateStopping)
	close(mgr.done)
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			ss.Stop(ctx)
		}
	}
	mgr.setState(box.StartStateStopped)
}

// Refresh internal box data.
func (mgr *Manager) Refresh(ctx context.Context) error {
	mgr.mgrLog.Debug().Msg("Refresh")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil
}

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

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

	st.ReadOnly = true
	sumZettel := 0
	for _, sst := range subStats {
		if !sst.ReadOnly {
			st.ReadOnly = false
		}
		sumZettel += sst.Zettel
	}
	st.NumManagedBoxes = len(mgr.boxes)
	st.ZettelTotal = sumZettel

	var storeSt store.Stats
	mgr.idxMx.RLock()
	defer mgr.idxMx.RUnlock()
	mgr.idxStore.ReadStats(&storeSt)

	st.LastReload = mgr.idxLastReload
	st.IndexesSinceReload = mgr.idxSinceReload
	st.DurLastReload = mgr.idxDurReload
	st.ZettelIndexed = storeSt.Zettel
	st.IndexUpdates = storeSt.Updates
	st.IndexedWords = storeSt.Words
	st.IndexedUrls = storeSt.Urls
}

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

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

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

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

type stringRefs map[string]id.Slice

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

	// Stats
	mxStats sync.Mutex
	updates uint64
}

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

func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	if zi, found := ms.idx[zid]; found && zi.meta != nil {
		// zi.meta is nil, if zettel was referenced, but is not indexed yet.
		return zi.meta.Clone(), nil
	}
	return nil, box.ErrZettelNotFound{Zid: zid}
}

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

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

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

	addBackwardZids(result, zid, zi)
	return result
}

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

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

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

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

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

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

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

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

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

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

	return toCheck
}

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

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

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

func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta {
	origM := zidx.GetMeta()
	copyM := meta.New(origM.Zid)
	for _, p := range origM.Pairs() {
		key := ms.internString(p.Key)
		if isInternableValue(key) {
			copyM.Set(key, ms.internString(p.Value))
		} else if key == api.KeyBoxNumber || !meta.IsComputed(key) {
			copyM.Set(key, p.Value)
		}
	}
	return copyM
}

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

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

	var toCheck id.Set
	for _, ref := range remRefs {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	}
	for _, ref := range newRefs {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	}
	return toCheck
}

func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set {
	// Must only be called if ms.mx is write-locked!
	inverseRefs := zidx.GetInverseRefs()
	for key, mr := range zi.otherRefs {
		if _, ok := inverseRefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}
	if zi.otherRefs == nil {
		zi.otherRefs = make(map[string]bidiRefs)
	}
	var toCheck id.Set
	for key, mrefs := range inverseRefs {
		mr := zi.otherRefs[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.otherRefs[key] = mr

		for _, ref := range newRefs {
			bzi := ms.getOrCreateEntry(ref)
			if bzi.otherRefs == nil {
				bzi.otherRefs = make(map[string]bidiRefs)
			}
			bmr := bzi.otherRefs[key]
			bmr.backward = addRef(bmr.backward, zidx.Zid)
			bzi.otherRefs[key] = bmr
			if bzi.meta == nil {
				toCheck = toCheck.Add(ref)
			}
		}
		ms.removeInverseMeta(zidx.Zid, key, remRefs)
	}
	return toCheck
}

func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {
		if refs, ok := srefs[word]; ok {
			srefs[word] = addRef(refs, zid)
			continue
		}
		srefs[word] = id.Slice{zid}
	}
	for _, word := range removeWords {
		refs, ok := srefs[word]
		if !ok {
			continue
		}
		refs2 := remRef(refs, zid)
		if len(refs2) == 0 {
			delete(srefs, word)
			continue
		}
		srefs[word] = refs2
	}
	return next.Words()
}

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

func (ms *memStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()

	curZi, curFound := ms.idx[curZid]
	_, newFound := ms.idx[newZid]
	if !curFound || newFound {
		return nil
	}
	newZi := &zettelData{
		meta:      copyMeta(curZi.meta, newZid),
		dead:      ms.copyDeadReferences(curZi.dead),
		forward:   ms.copyForward(curZi.forward, newZid),
		backward:  nil, // will be done through tocheck
		otherRefs: nil, // TODO: check if this will be done through toCheck
		words:     copyStrings(ms.words, curZi.words, newZid),
		urls:      copyStrings(ms.urls, curZi.urls, newZid),
	}

	ms.idx[newZid] = newZi
	toCheck := ms.doDeleteZettel(curZid)
	toCheck = toCheck.CopySlice(ms.dead[newZid])
	delete(ms.dead, newZid)
	toCheck = toCheck.Add(newZid) // should update otherRefs
	return toCheck
}
func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta {
	result := m.Clone()
	result.Zid = newZid
	return result
}
func (ms *memStore) copyDeadReferences(curDead id.Slice) id.Slice {
	// Must only be called if ms.mx is write-locked!
	if l := len(curDead); l > 0 {
		result := make(id.Slice, l)
		for i, ref := range curDead {
			result[i] = ref
			ms.dead[ref] = addRef(ms.dead[ref], ref)
		}
		return result
	}
	return nil
}
func (ms *memStore) copyForward(curForward id.Slice, newZid id.Zid) id.Slice {
	// Must only be called if ms.mx is write-locked!
	if l := len(curForward); l > 0 {
		result := make(id.Slice, l)
		for i, ref := range curForward {
			result[i] = ref
			if fzi, found := ms.idx[ref]; found {
				fzi.backward = addRef(fzi.backward, newZid)
			}
		}
		return result
	}
	return nil
}
func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string {
	// Must only be called if ms.mx is write-locked!
	if l := len(curStrings); l > 0 {
		result := make([]string, l)
		for i, s := range curStrings {
			result[i] = s
			msStringMap[s] = addRef(msStringMap[s], newZid)
		}
		return result
	}
	return nil
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

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

package mapstore

import (
	"slices"

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

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

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

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

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

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

package mapstore

import (
	"testing"

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

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

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

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

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

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

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

// Package store contains general index data for storing a zettel index.
package store

import (
	"context"
	"io"

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

// Stats records statistics about the store.
type Stats struct {
	// Zettel is the number of zettel managed by the indexer.
	Zettel int

	// Updates count the number of metadata updates.
	Updates uint64

	// Words count the different words stored in the store.
	Words uint64

	// Urls count the different URLs stored in the store.
	Urls uint64
}

// Store all relevant zettel data. There may be multiple implementations, i.e.
// memory-based, file-based, based on SQLite, ...
type Store interface {
	query.Searcher

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

	// Entrich metadata with data from store.
	Enrich(ctx context.Context, m *meta.Meta)

	// UpdateReferences for a specific zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	UpdateReferences(context.Context, *ZettelIndex) id.Set

	// RenameZettel changes all references of current zettel identifier to new
	// zettel identifier.
	RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set

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

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

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

Added box/manager/store/wordset.go.
































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// 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

// NewWordSet returns a new WordSet.
func NewWordSet() WordSet { return make(WordSet) }

// Add one word to the set
func (ws WordSet) Add(s string) {
	ws[s] = ws[s] + 1
}

// Words gives the slice of all words in the set.
func (ws WordSet) Words() []string {
	if len(ws) == 0 {
		return nil
	}
	words := make([]string, 0, len(ws))
	for w := range ws {
		words = append(words, w)
	}
	return words
}

// Diff calculates the word slice to be added and to be removed from oldWords
// to get the given word set.
func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) {
	if len(ws) == 0 {
		return nil, oldWords
	}
	if len(oldWords) == 0 {
		return ws.Words(), nil
	}
	oldSet := make(WordSet, len(oldWords))
	for _, ow := range oldWords {
		if _, ok := ws[ow]; ok {
			oldSet[ow] = 1
			continue
		}
		removeWords = append(removeWords, ow)
	}
	for w := range ws {
		if _, ok := oldSet[w]; ok {
			continue
		}
		newWords = append(newWords, w)
	}
	return newWords, removeWords
}

Added 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"testing"

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

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

func TestWordsWords(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		words store.WordSet
		exp   []string
	}{
		{nil, nil},
		{store.WordSet{}, nil},
		{store.WordSet{"a": 1, "b": 2}, []string{"a", "b"}},
	}
	for i, tc := range testcases {
		got := tc.words.Words()
		if !equalWordList(tc.exp, got) {
			t.Errorf("%d: %v.Words() == %v, but got %v", i, tc.words, tc.exp, got)
		}
	}
}

func TestWordsDiff(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		cur        store.WordSet
		old        []string
		expN, expR []string
	}{
		{nil, nil, nil, nil},
		{store.WordSet{}, []string{}, nil, nil},
		{store.WordSet{"a": 1}, []string{}, []string{"a"}, nil},
		{store.WordSet{"a": 1}, []string{"b"}, []string{"a"}, []string{"b"}},
		{store.WordSet{}, []string{"b"}, nil, []string{"b"}},
		{store.WordSet{"a": 1}, []string{"a"}, nil, nil},
	}
	for i, tc := range testcases {
		gotN, gotR := tc.cur.Diff(tc.old)
		if !equalWordList(tc.expN, gotN) {
			t.Errorf("%d: %v.Diff(%v)->new %v, but got %v", i, tc.cur, tc.old, tc.expN, gotN)
		}
		if !equalWordList(tc.expR, gotR) {
			t.Errorf("%d: %v.Diff(%v)->rem %v, but got %v", i, tc.cur, tc.old, tc.expR, gotR)
		}
	}
}

Added box/manager/store/zettel.go.
































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package store

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Added 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 membox stores zettel volatile in main memory.
package membox

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

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

func init() {
	manager.Register(
		"mem",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &memBox{
				log: kernel.Main.GetLogger(kernel.BoxService).Clone().
					Str("box", "mem").Int("boxnum", int64(cdata.Number)).Child(),
				u:         u,
				cdata:     *cdata,
				maxZettel: box.GetQueryInt(u, "max-zettel", 0, 127, 65535),
				maxBytes:  box.GetQueryInt(u, "max-bytes", 0, 65535, (1024*1024*1024)-1),
			}, nil
		})
}

type memBox struct {
	log       *logger.Logger
	u         *url.URL
	cdata     manager.ConnectData
	maxZettel int
	maxBytes  int
	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]zettel.Zettel
	curBytes  int
}

func (mb *memBox) notifyChanged(zid id.Zid) {
	if chci := mb.cdata.Notify; chci != nil {
		chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid}
	}
}

func (mb *memBox) Location() string {
	return mb.u.String()
}

func (mb *memBox) State() box.StartState {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	if mb.zettel == nil {
		return box.StartStateStopped
	}
	return box.StartStateStarted
}

func (mb *memBox) Start(context.Context) error {
	mb.mx.Lock()
	mb.zettel = make(map[id.Zid]zettel.Zettel)
	mb.curBytes = 0
	mb.mx.Unlock()
	mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box")
	return nil
}

func (mb *memBox) Stop(context.Context) {
	mb.mx.Lock()
	mb.zettel = nil
	mb.mx.Unlock()
}

func (mb *memBox) CanCreateZettel(context.Context) bool {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	return len(mb.zettel) < mb.maxZettel
}

func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) {
	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return id.Invalid, box.ErrCapacity
	}
	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
		_, ok := mb.zettel[zid]
		return !ok, nil
	})
	if err != nil {
		mb.mx.Unlock()
		return id.Invalid, err
	}
	meta := zettel.Meta.Clone()
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	mb.log.Trace().Zid(zid).Msg("CreateZettel")
	return zid, nil
}

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

func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, found := mb.zettel[zid]
	mb.mx.RUnlock()
	return found
}

func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid")
	for zid := range mb.zettel {
		if constraint(zid) {
			handle(zid)
		}
	}
	return nil
}

func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyMeta")
	for zid, zettel := range mb.zettel {
		if constraint(zid) {
			m := zettel.Meta.Clone()
			mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number)
			handle(m)
		}
	}
	return nil
}

func (mb *memBox) CanUpdateZettel(_ context.Context, zettel zettel.Zettel) bool {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	zid := zettel.Meta.Zid
	if !zid.IsValid() {
		return false
	}

	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[zid]; found {
		newBytes -= prevZettel.Length()
	}
	return newBytes < mb.maxBytes
}

func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error {
	m := zettel.Meta.Clone()
	if !m.Zid.IsValid() {
		return box.ErrInvalidZid{Zid: m.Zid.String()}
	}

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

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid)
	mb.log.Trace().Msg("UpdateZettel")
	return nil
}

func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true }

func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error {
	mb.mx.Lock()
	zettel, ok := mb.zettel[curZid]
	if !ok {
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: curZid}
	}

	// Check that there is no zettel with newZid
	if _, ok = mb.zettel[newZid]; ok {
		mb.mx.Unlock()
		return box.ErrInvalidZid{Zid: newZid.String()}
	}

	meta := zettel.Meta.Clone()
	meta.Zid = newZid
	zettel.Meta = meta
	mb.zettel[newZid] = zettel
	delete(mb.zettel, curZid)
	mb.mx.Unlock()
	mb.notifyChanged(curZid)
	mb.notifyChanged(newZid)
	mb.log.Trace().Msg("RenameZettel")
	return nil
}

func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	return ok
}

func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	mb.mx.Lock()
	oldZettel, found := mb.zettel[zid]
	if !found {
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	mb.log.Trace().Msg("DeleteZettel")
	return nil
}

func (mb *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mb.mx.RLock()
	st.Zettel = len(mb.zettel)
	mb.mx.RUnlock()
	mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type entrySet map[id.Zid]*DirEntry

// DirServiceState signal the internal state of the service.
//
// The following state transitions are possible:
// --newDirService--> dsCreated
// dsCreated --Start--> dsStarting
// dsStarting --last list notification--> dsWorking
// dsWorking --directory missing--> dsMissing
// dsMissing --last list notification--> dsWorking
// --Stop--> dsStopping
type DirServiceState uint8

const (
	DsCreated  DirServiceState = iota
	DsStarting                 // Reading inital scan
	DsWorking                  // Initial scan complete, fully operational
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    chan<- box.UpdateInfo
	mx       sync.RWMutex // protects status, entries
	state    DirServiceState
	entries  entrySet
}

// ErrNoDirectory signals missing directory data.
var ErrNoDirectory = errors.New("unable to retrieve zettel directory information")

// NewDirService creates a new directory service.
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    chci,
		state:    DsCreated,
	}
}

// State the current service state.
func (ds *DirService) State() DirServiceState {
	ds.mx.RLock()
	state := ds.state
	ds.mx.RUnlock()
	return state
}

// Start the directory service.
func (ds *DirService) Start() {
	ds.mx.Lock()
	ds.state = DsStarting
	ds.mx.Unlock()
	var newEntries entrySet
	go ds.updateEvents(newEntries)
}

// Refresh the directory entries.
func (ds *DirService) Refresh() {
	ds.notifier.Refresh()
}

// Stop the directory service.
func (ds *DirService) Stop() {
	ds.mx.Lock()
	ds.state = DsStopping
	ds.mx.Unlock()
	ds.notifier.Close()
}

func (ds *DirService) logMissingEntry(action string) error {
	err := ErrNoDirectory
	ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information")
	return err
}

// NumDirEntries returns the number of entries in the directory.
func (ds *DirService) NumDirEntries() int {
	ds.mx.RLock()
	defer ds.mx.RUnlock()
	if ds.entries == nil {
		return 0
	}
	return len(ds.entries)
}

// GetDirEntries returns a list of directory entries, which satisfy the given constraint.
func (ds *DirService) GetDirEntries(constraint query.RetrievePredicate) []*DirEntry {
	ds.mx.RLock()
	defer ds.mx.RUnlock()
	if ds.entries == nil {
		return nil
	}
	result := make([]*DirEntry, 0, len(ds.entries))
	for zid, entry := range ds.entries {
		if constraint(zid) {
			copiedEntry := *entry
			result = append(result, &copiedEntry)
		}
	}
	return result
}

// GetDirEntry returns a directory entry with the given zid, or nil if not found.
func (ds *DirService) GetDirEntry(zid id.Zid) *DirEntry {
	ds.mx.RLock()
	defer ds.mx.RUnlock()
	if ds.entries == nil {
		return nil
	}
	foundEntry := ds.entries[zid]
	if foundEntry == nil {
		return nil
	}
	result := *foundEntry
	return &result
}

// SetNewDirEntry calculates an empty directory entry with an unused identifier and
// stores it in the directory.
func (ds *DirService) SetNewDirEntry() (id.Zid, error) {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return id.Invalid, ds.logMissingEntry("new")
	}
	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
		_, found := ds.entries[zid]
		return !found, nil
	})
	if err != nil {
		return id.Invalid, err
	}
	ds.entries[zid] = &DirEntry{Zid: zid}
	return zid, nil
}

// UpdateDirEntry updates an directory entry in place.
func (ds *DirService) UpdateDirEntry(updatedEntry *DirEntry) error {
	entry := *updatedEntry
	ds.mx.Lock()
	defer ds.mx.Unlock()
	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.ErrInvalidZid{Zid: newZid.String()}
	}
	oldZid := oldEntry.Zid
	newEntry := DirEntry{
		Zid:         newZid,
		MetaName:    renameFilename(oldEntry.MetaName, oldZid, newZid),
		ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid),
		ContentExt:  oldEntry.ContentExt,
		// 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 ri := recover(); ri != nil {
			kernel.Main.LogRecover("DirectoryService", ri)
			go ds.updateEvents(newEntries)
		}
	}()

	for ev := range ds.notifier.Events() {
		e, ok := ds.handleEvent(ev, newEntries)
		if !ok {
			break
		}
		newEntries = e
	}
}
func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) {
	ds.mx.RLock()
	state := ds.state
	ds.mx.RUnlock()

	if msg := ds.log.Trace(); msg.Enabled() {
		msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent")
	}
	if state == DsStopping {
		return nil, false
	}

	switch ev.Op {
	case Error:
		newEntries = nil
		if state != DsMissing {
			ds.log.Error().Err(ev.Err).Msg("Notifier confused")
		}
	case Make:
		newEntries = make(entrySet)
	case List:
		if ev.Name == "" {
			zids := getNewZids(newEntries)
			ds.mx.Lock()
			fromMissing := ds.state == DsMissing
			prevEntries := ds.entries
			ds.entries = newEntries
			ds.state = DsWorking
			ds.mx.Unlock()
			ds.onCreateDirectory(zids, prevEntries)
			if fromMissing {
				ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found")
			}
			return nil, true
		}
		if newEntries != nil {
			ds.onUpdateFileEvent(newEntries, ev.Name)
		}
	case Destroy:
		ds.onDestroyDirectory()
		ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing")
		return nil, true
	case Update:
		ds.mx.Lock()
		zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid)
		}
	case Delete:
		ds.mx.Lock()
		zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid)
		}
	default:
		ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
	}
	return newEntries, true
}

func getNewZids(entries entrySet) id.Slice {
	zids := make(id.Slice, 0, len(entries))
	for zid := range entries {
		zids = append(zids, zid)
	}
	return zids
}

func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) {
	for _, zid := range zids {
		ds.notifyChange(zid)
		delete(prevEntries, zid)
	}

	// These were previously stored, by are not found now.
	// Notify system that these were deleted, e.g. for updating the index.
	for zid := range prevEntries {
		ds.notifyChange(zid)
	}
}

func (ds *DirService) onDestroyDirectory() {
	ds.mx.Lock()
	entries := ds.entries
	ds.entries = nil
	ds.state = DsMissing
	ds.mx.Unlock()
	for zid := range entries {
		ds.notifyChange(zid)
	}
}

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

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
}

func seekZid(name string) id.Zid {
	match := matchValidFileName(name)
	if len(match) == 0 {
		return id.Invalid
	}
	zid, err := id.Parse(match[1])
	if err != nil {
		return id.Invalid
	}
	return zid
}

func fetchdirEntry(entries entrySet, zid id.Zid) *DirEntry {
	if entry, found := entries[zid]; found {
		return entry
	}
	entry := &DirEntry{Zid: zid}
	entries[zid] = entry
	return entry
}

func (ds *DirService) onUpdateFileEvent(entries entrySet, name string) id.Zid {
	if entries == nil {
		return id.Invalid
	}
	zid := seekZid(name)
	if zid == id.Invalid {
		return id.Invalid
	}
	entry := fetchdirEntry(entries, zid)
	dupName1, dupName2 := ds.updateEntry(entry, name)
	if dupName1 != "" {
		ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)")
		if dupName2 != "" {
			ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)")
		}
		return id.Invalid
	}
	return zid
}

func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid {
	if entries == nil {
		return id.Invalid
	}
	zid := seekZid(name)
	if zid == id.Invalid {
		return id.Invalid
	}
	entry, found := entries[zid]
	if !found {
		return zid
	}
	for i, dupName := range entry.UselessFiles {
		if dupName == name {
			removeDuplicate(entry, i)
			return zid
		}
	}
	if name == entry.ContentName {
		entry.ContentName = ""
		entry.ContentExt = ""
		ds.replayUpdateUselessFiles(entry)
	} else if name == entry.MetaName {
		entry.MetaName = ""
		ds.replayUpdateUselessFiles(entry)
	}
	if entry.ContentName == "" && entry.MetaName == "" {
		delete(entries, zid)
	}
	return zid
}

func removeDuplicate(entry *DirEntry, i int) {
	if len(entry.UselessFiles) == 1 {
		entry.UselessFiles = nil
		return
	}
	entry.UselessFiles = entry.UselessFiles[:i+copy(entry.UselessFiles[i:], entry.UselessFiles[i+1:])]
}

func (ds *DirService) replayUpdateUselessFiles(entry *DirEntry) {
	uselessFiles := entry.UselessFiles
	if len(uselessFiles) == 0 {
		return
	}
	entry.UselessFiles = make([]string, 0, len(uselessFiles))
	for _, name := range uselessFiles {
		ds.updateEntry(entry, name)
	}
	if len(uselessFiles) == len(entry.UselessFiles) {
		return
	}
loop:
	for _, prevName := range uselessFiles {
		for _, newName := range entry.UselessFiles {
			if prevName == newName {
				continue loop
			}
		}
		ds.log.Info().Str("name", prevName).Msg("Previous duplicate file becomes useful")
	}
}

func (ds *DirService) updateEntry(entry *DirEntry, name string) (string, string) {
	ext := onlyExt(name)
	if !extIsMetaAndContent(entry.ContentExt) {
		if ext == "" {
			return updateEntryMeta(entry, name), ""
		}
		if entry.MetaName == "" {
			if nameWithoutExt(name, ext) == entry.ContentName {
				// We have marked a file as content file, but it is a metadata file,
				// because it is the same as the new file without extension.
				entry.MetaName = entry.ContentName
				entry.ContentName = ""
				entry.ContentExt = ""
				ds.replayUpdateUselessFiles(entry)
			} else if entry.ContentName != "" && nameWithoutExt(entry.ContentName, entry.ContentExt) == name {
				// We have already a valid content file, and new file should serve as metadata file,
				// because it is the same as the content file without extension.
				entry.MetaName = name
				return "", ""
			}
		}
	}
	return updateEntryContent(entry, name, ext)
}

func nameWithoutExt(name, ext string) string {
	return name[0 : len(name)-len(ext)-1]
}

func updateEntryMeta(entry *DirEntry, name string) string {
	metaName := entry.MetaName
	if metaName == "" {
		entry.MetaName = name
		return ""
	}
	if metaName == name {
		return ""
	}
	if newNameIsBetter(metaName, name) {
		entry.MetaName = name
		return addUselessFile(entry, metaName)
	}
	return addUselessFile(entry, name)
}

func updateEntryContent(entry *DirEntry, name, ext string) (string, string) {
	contentName := entry.ContentName
	if contentName == "" {
		entry.ContentName = name
		entry.ContentExt = ext
		return "", ""
	}
	if contentName == name {
		return "", ""
	}
	contentExt := entry.ContentExt
	if contentExt == ext {
		if newNameIsBetter(contentName, name) {
			entry.ContentName = name
			return addUselessFile(entry, contentName), ""
		}
		return addUselessFile(entry, name), ""
	}
	if contentExt == extZettel {
		return addUselessFile(entry, name), ""
	}
	if ext == extZettel {
		entry.ContentName = name
		entry.ContentExt = ext
		contentName = addUselessFile(entry, contentName)
		if metaName := entry.MetaName; metaName != "" {
			metaName = addUselessFile(entry, metaName)
			entry.MetaName = ""
			return contentName, metaName
		}
		return contentName, ""
	}
	if newExtIsBetter(contentExt, ext) {
		entry.ContentName = name
		entry.ContentExt = ext
		return addUselessFile(entry, contentName), ""
	}
	return addUselessFile(entry, name), ""
}
func addUselessFile(entry *DirEntry, name string) string {
	for _, dupName := range entry.UselessFiles {
		if name == dupName {
			return ""
		}
	}
	entry.UselessFiles = append(entry.UselessFiles, name)
	return name
}

func onlyExt(name string) string {
	ext := filepath.Ext(name)
	if ext == "" || ext[0] != '.' {
		return ext
	}
	return ext[1:]
}

func newNameIsBetter(oldName, newName string) bool {
	if len(oldName) < len(newName) {
		return false
	}
	return oldName > newName
}

var supportedSyntax, primarySyntax strfun.Set

func init() {
	syntaxList := parser.GetSyntaxes()
	supportedSyntax = strfun.NewSet(syntaxList...)
	primarySyntax = make(map[string]struct{}, len(syntaxList))
	for _, syntax := range syntaxList {
		if parser.Get(syntax).Name == syntax {
			primarySyntax.Set(syntax)
		}
	}
}
func newExtIsBetter(oldExt, newExt string) bool {
	oldSyntax := supportedSyntax.Has(oldExt)
	if oldSyntax != supportedSyntax.Has(newExt) {
		return !oldSyntax
	}
	if oldSyntax {
		if oldExt == "zmk" {
			return false
		}
		if newExt == "zmk" {
			return true
		}
		oldInfo := parser.Get(oldExt)
		newInfo := parser.Get(newExt)
		if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser {
			return !oldASTParser
		}
		if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat {
			return !oldTextFormat
		}
		if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat {
			return oldImageFormat
		}
		if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) {
			return !oldPrimary
		}
	}

	oldLen := len(oldExt)
	newLen := len(newExt)
	if oldLen != newLen {
		return newLen < oldLen
	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(zid id.Zid) {
	if chci := ds.infos; chci != nil {
		ds.log.Trace().Zid(zid).Msg("notifyChange")
		chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid}
	}
}

Added 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

import (
	"testing"

	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func TestSeekZid(t *testing.T) {
	testcases := []struct {
		name string
		zid  id.Zid
	}{
		{"", id.Invalid},
		{"1", id.Invalid},
		{"1234567890123", id.Invalid},
		{" 12345678901234", id.Invalid},
		{"12345678901234", id.Zid(12345678901234)},
		{"12345678901234.ext", id.Zid(12345678901234)},
		{"12345678901234 abc.ext", id.Zid(12345678901234)},
		{"12345678901234.abc.ext", id.Zid(12345678901234)},
		{"12345678901234 def", id.Zid(12345678901234)},
	}
	for _, tc := range testcases {
		gotZid := seekZid(tc.name)
		if gotZid != tc.zid {
			t.Errorf("seekZid(%q) == %v, but got %v", tc.name, tc.zid, gotZid)
		}
	}
}

func TestNewExtIsBetter(t *testing.T) {
	extVals := []string{
		// Main Formats
		meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD,
		// Other supported text formats
		meta.SyntaxCSS, meta.SyntaxSxn, meta.SyntaxTxt, meta.SyntaxHTML,
		meta.SyntaxText, meta.SyntaxPlain,
		// Supported text graphics formats
		meta.SyntaxSVG,
		meta.SyntaxNone,
		// Supported binary graphic formats
		meta.SyntaxGif, meta.SyntaxPNG, meta.SyntaxJPEG, meta.SyntaxWebp, meta.SyntaxJPG,

		// Unsupported syntax values
		"gz", "cpp", "tar", "cppc",
	}
	for oldI, oldExt := range extVals {
		for newI, newExt := range extVals {
			if oldI <= newI {
				continue
			}
			if !newExtIsBetter(oldExt, newExt) {
				t.Errorf("newExtIsBetter(%q, %q) == true, but got false", oldExt, newExt)
			}
			if newExtIsBetter(newExt, oldExt) {
				t.Errorf("newExtIsBetter(%q, %q) == false, but got true", newExt, oldExt)
			}
		}
	}
}

Added 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 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/client.fossil/api"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const (
	extZettel = "zettel" // file contains metadata and content
	extBin    = "bin"    // file contains binary content
	extTxt    = "txt"    // file contains non-binary content
)

func extIsMetaAndContent(ext string) bool { return ext == extZettel }

// DirEntry stores everything for a directory entry.
type DirEntry struct {
	Zid          id.Zid
	MetaName     string   // file name of meta information
	ContentName  string   // file name of zettel content
	ContentExt   string   // (normalized) file extension of zettel content
	UselessFiles []string // list of other content files
}

// IsValid checks whether the entry is valid.
func (e *DirEntry) IsValid() bool {
	return e != nil && e.Zid.IsValid()
}

// HasMetaInContent returns true, if metadata will be stored in the content file.
func (e *DirEntry) HasMetaInContent() bool {
	return e.IsValid() && extIsMetaAndContent(e.ContentExt)
}

// SetupFromMetaContent fills entry data based on metadata and zettel content.
func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []string) {
	if e.Zid != m.Zid {
		panic("Zid differ")
	}
	if contentName := e.ContentName; contentName != "" {
		if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" {
			e.MetaName = e.calcBaseName(contentName)
		}
		return
	}

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

func contentExtWithMeta(syntax string, content zettel.Content) string {
	p := parser.Get(syntax)
	if content.IsBinary() {
		if p.IsImageFormat {
			return syntax
		}
		return extBin
	}
	if p.IsImageFormat {
		return extTxt
	}
	return syntax
}

func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string {
	if yamlSep {
		return extZettel
	}
	switch syntax {
	case meta.SyntaxNone, meta.SyntaxZmk:
		return extZettel
	}
	for _, s := range getZettelFileSyntax() {
		if s == syntax {
			return extZettel
		}
	}
	return syntax

}

func (e *DirEntry) calcBaseName(name string) string {
	if name == "" {
		return e.Zid.String()
	}
	return name[0 : len(name)-len(filepath.Ext(name))]

}

Added box/notify/fsdir.go.











































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 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"
	"strings"

	"github.com/fsnotify/fsnotify"
	"zettelstore.de/z/logger"
)

type fsdirNotifier struct {
	log     *logger.Logger
	events  chan Event
	done    chan struct{}
	refresh chan struct{}
	base    *fsnotify.Watcher
	path    string
	fetcher EntryFetcher
	parent  string
}

// NewFSDirNotifier creates a directory based notifier that receives notifications
// from the file system.
func NewFSDirNotifier(log *logger.Logger, path string) (Notifier, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		log.Debug().Err(err).Str("path", path).Msg("Unable to create absolute path")
		return nil, err
	}
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Debug().Err(err).Str("absPath", absPath).Msg("Unable to create watcher")
		return nil, err
	}
	absParentDir := filepath.Dir(absPath)
	errParent := watcher.Add(absParentDir)
	err = watcher.Add(absPath)
	if errParent != nil {
		if err != nil {
			log.Error().
				Str("parentDir", absParentDir).Err(errParent).
				Str("path", absPath).Err(err).
				Msg("Unable to access Zettel directory and its parent directory")
			watcher.Close()
			return nil, err
		}
		log.Info().Str("parentDir", absParentDir).Err(errParent).
			Msg("Parent of Zettel directory cannot be supervised")
		log.Info().Str("path", absPath).
			Msg("Zettelstore might not detect a deletion or movement of the Zettel directory")
	} else if err != nil {
		// Not a problem, if container is not available. It might become available later.
		log.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available")
	}

	fsdn := &fsdirNotifier{
		log:     log,
		events:  make(chan Event),
		refresh: make(chan struct{}),
		done:    make(chan struct{}),
		base:    watcher,
		path:    absPath,
		fetcher: newDirPathFetcher(absPath),
		parent:  absParentDir,
	}
	go fsdn.eventLoop()
	return fsdn, nil
}

func (fsdn *fsdirNotifier) Events() <-chan Event {
	return fsdn.events
}

func (fsdn *fsdirNotifier) Refresh() {
	fsdn.refresh <- struct{}{}
}

func (fsdn *fsdirNotifier) eventLoop() {
	defer fsdn.base.Close()
	defer close(fsdn.events)
	defer close(fsdn.refresh)
	if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) {
		return
	}

	for fsdn.readAndProcessEvent() {
	}
}

func (fsdn *fsdirNotifier) readAndProcessEvent() bool {
	select {
	case <-fsdn.done:
		fsdn.traceDone(1)
		return false
	default:
	}
	select {
	case <-fsdn.done:
		fsdn.traceDone(2)
		return false
	case <-fsdn.refresh:
		fsdn.log.Trace().Msg("refresh")
		listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done)
	case err, ok := <-fsdn.base.Errors:
		fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors")
		if !ok {
			return false
		}
		select {
		case fsdn.events <- Event{Op: Error, Err: err}:
		case <-fsdn.done:
			fsdn.traceDone(3)
			return false
		}
	case ev, ok := <-fsdn.base.Events:
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event")
		if !ok {
			return false
		}
		if !fsdn.processEvent(&ev) {
			return false
		}
	}
	return true
}

func (fsdn *fsdirNotifier) traceDone(pos int64) {
	fsdn.log.Trace().Int("i", pos).Msg("done with read and process events")
}

func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool {
	if strings.HasPrefix(ev.Name, fsdn.path) {
		if len(ev.Name) == len(fsdn.path) {
			return fsdn.processDirEvent(ev)
		}
		return fsdn.processFileEvent(ev)
	}
	fsdn.log.Trace().Str("path", fsdn.path).Str("name", ev.Name).Str("op", ev.Op.String()).Msg("event does not match")
	return true
}

func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool {
	if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) {
		fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed")
		fsdn.base.Remove(fsdn.path)
		select {
		case fsdn.events <- Event{Op: Destroy}:
		case <-fsdn.done:
			fsdn.log.Trace().Int("i", 1).Msg("done dir event processing")
			return false
		}
		return true
	}

	if ev.Has(fsnotify.Create) {
		err := fsdn.base.Add(fsdn.path)
		if err != nil {
			fsdn.log.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
			}
		}
		fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added")
		return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done)
	}

	fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed")
	return true
}

func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool {
	if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
		if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() {
			regular := err == nil && fi.Mode().IsRegular()
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Err(err).Bool("regular", regular).Msg("error with file")
			return true
		}
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated")
		return fsdn.sendEvent(Update, filepath.Base(ev.Name))
	}

	if ev.Has(fsnotify.Rename) {
		fi, err := os.Lstat(ev.Name)
		if err != nil {
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted")
			return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
		}
		if fi.Mode().IsRegular() {
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated")
			return fsdn.sendEvent(Update, filepath.Base(ev.Name))
		}
		fsdn.log.Trace().Str("name", ev.Name).Msg("File not regular")
		return true
	}

	if ev.Has(fsnotify.Remove) {
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted")
		return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
	}

	fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed")
	return true
}

func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool {
	select {
	case fsdn.events <- Event{Op: op, Name: filename}:
	case <-fsdn.done:
		fsdn.log.Trace().Msg("done file event processing")
		return false
	}
	return true
}

func (fsdn *fsdirNotifier) Close() {
	close(fsdn.done)
}

Added 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

import (
	"archive/zip"
	"os"

	"zettelstore.de/z/logger"
)

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

type dirPathFetcher struct {
	dirPath string
}

func newDirPathFetcher(dirPath string) EntryFetcher { return &dirPathFetcher{dirPath} }

func (dpf *dirPathFetcher) Fetch() ([]string, error) {
	entries, err := os.ReadDir(dpf.dirPath)
	if err != nil {
		return nil, err
	}
	result := make([]string, 0, len(entries))
	for _, entry := range entries {
		if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() {
			continue
		}
		result = append(result, entry.Name())
	}
	return result, nil
}

type zipPathFetcher struct {
	zipPath string
}

func newZipPathFetcher(zipPath string) EntryFetcher { return &zipPathFetcher{zipPath} }

func (zpf *zipPathFetcher) Fetch() ([]string, error) {
	reader, err := zip.OpenReader(zpf.zipPath)
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	result := make([]string, 0, len(reader.File))
	for _, f := range reader.File {
		result = append(result, f.Name)
	}
	return result, nil
}

// listDirElements write all files within the directory path as events.
func listDirElements(log *logger.Logger, fetcher EntryFetcher, events chan<- Event, done <-chan struct{}) bool {
	select {
	case events <- Event{Op: Make}:
	case <-done:
		return false
	}
	entries, err := fetcher.Fetch()
	if err != nil {
		select {
		case events <- Event{Op: Error, Err: err}:
		case <-done:
			return false
		}
	}
	for _, name := range entries {
		log.Trace().Str("name", name).Msg("File listed")
		select {
		case events <- Event{Op: List, Name: name}:
		case <-done:
			return false
		}
	}

	select {
	case events <- Event{Op: List}:
	case <-done:
		return false
	}
	return true
}

Added box/notify/notify.go.






















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

// Notifier send events about their container and content.
type Notifier interface {
	// Return the channel
	Events() <-chan Event

	// Signal a refresh of the container. This will result in some events.
	Refresh()

	// Close the notifier (and eventually the channel)
	Close()
}

// EventOp describe a notification operation.
type EventOp uint8

// Valid constants for event operations.
//
// Error signals a detected error. Details are in Event.Err.
//
// Make signals that the container is detected. List events will follow.
//
// List signals a found file, if Event.Name is not empty. Otherwise it signals
// the end of files within the container.
//
// Destroy signals that the container is not there any more. It might me Make later again.
//
// Update signals that file Event.Name was created/updated.
// File name is relative to the container.
//
// Delete signals that file Event.Name was removed.
// File name is relative to the container's name.
const (
	_       EventOp = iota
	Error           // Error while operating
	Make            // Make container
	List            // List container
	Destroy         // Destroy container
	Update          // Update element
	Delete          // Delete element
)

// String representation of operation code.
func (c EventOp) String() string {
	switch c {
	case Error:
		return "ERROR"
	case Make:
		return "MAKE"
	case List:
		return "LIST"
	case Destroy:
		return "DESTROY"
	case Update:
		return "UPDATE"
	case Delete:
		return "DELETE"
	default:
		return fmt.Sprintf("UNKNOWN(%d)", c)
	}
}

// Event represents a single container / element event.
type Event struct {
	Op   EventOp
	Name string
	Err  error // Valid iff Op == Error
}

Added box/notify/simpledir.go.

























































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/z/logger"
)

type simpleDirNotifier struct {
	log     *logger.Logger
	events  chan Event
	done    chan struct{}
	refresh chan struct{}
	fetcher EntryFetcher
}

// NewSimpleDirNotifier creates a directory based notifier that will not receive
// any notifications from the operating system.
func NewSimpleDirNotifier(log *logger.Logger, path string) (Notifier, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		return nil, err
	}
	sdn := &simpleDirNotifier{
		log:     log,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newDirPathFetcher(absPath),
	}
	go sdn.eventLoop()
	return sdn, nil
}

// NewSimpleZipNotifier creates a zip-file based notifier that will not receive
// any notifications from the operating system.
func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier {
	sdn := &simpleDirNotifier{
		log:     log,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newZipPathFetcher(zipPath),
	}
	go sdn.eventLoop()
	return sdn
}

func (sdn *simpleDirNotifier) Events() <-chan Event {
	return sdn.events
}

func (sdn *simpleDirNotifier) Refresh() {
	sdn.refresh <- struct{}{}
}

func (sdn *simpleDirNotifier) eventLoop() {
	defer close(sdn.events)
	defer close(sdn.refresh)
	if !listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) {
		return
	}
	for {
		select {
		case <-sdn.done:
			return
		case <-sdn.refresh:
			listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done)
		}
	}
}

func (sdn *simpleDirNotifier) Close() {
	close(sdn.done)
}

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
54
55
56
57
58
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
1

2
3

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






23
24
25
26
27
28
29
30
31
32
33


34
35
36
37
38
39
40
41

42
43

44
45

46
47
48



49
50
51
52
53

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76
77

78
79
80
81

82
83
84
85

86
87
88
89

-
+

-
+




+
+
+





+





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




-
-
+
+





+
-
+

-
+

-
+


-
-
-
+
+
+


-
+














-
+








-
+



-
+



-
+



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

package cmd

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

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/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, cfg *meta.Meta) (int, error) {
	format := fs.Lookup("t").Value.String()
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(meta.KeySyntax, meta.ValueSyntaxZmk),
		m.GetDefault(api.KeySyntax, meta.DefaultSyntax),
		nil,
	)
	enc := encoder.Create(format, &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)})
	if enc == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", format)
	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 = enc.WriteZettel(os.Stdout, z, format != "raw")
	_, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata)
	if err != nil {
		return 2, err
	}
	fmt.Println()

	return 0, nil
}

func getInput(args []string) (*meta.Meta, *input.Input, error) {
	if len(args) < 1 {
		src, err := io.ReadAll(os.Stdin)
		if err != nil {
			return nil, nil, err
		}
		inp := input.NewInput(string(src))
		inp := input.NewInput(src)
		m := meta.NewFromInput(id.New(true), inp)
		return m, inp, nil
	}

	src, err := os.ReadFile(args[0])
	if err != nil {
		return nil, nil, err
	}
	inp := input.NewInput(string(src))
	inp := input.NewInput(src)
	m := meta.NewFromInput(id.New(true), inp)

	if len(args) > 1 {
		src, err := os.ReadFile(args[1])
		src, err = os.ReadFile(args[1])
		if err != nil {
			return nil, nil, err
		}
		inp = input.NewInput(string(src))
		inp = input.NewInput(src)
	}
	return m, inp, nil
}

Changes to cmd/cmd_password.go.

1
2

3
4

5
6
7
8



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



23
24
25
26
27

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

package cmd

import (
	"flag"
	"fmt"
	"os"

	"golang.org/x/term"

	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/zettel/id"
)

// ---------- Subcommand: password -------------------------------------------

func cmdPassword(fs *flag.FlagSet, cfg *meta.Meta) (int, error) {
func cmdPassword(fs *flag.FlagSet) (int, error) {
	if fs.NArg() == 0 {
		fmt.Fprintln(os.Stderr, "User name and user zettel identification missing")
		return 2, nil
	}
	if fs.NArg() == 1 {
		fmt.Fprintln(os.Stderr, "User zettel identification missing")
		return 2, nil
56
57
58
59
60
61
62
63
64


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


66
67
68
69
70
71
72
73
74
75
76
77







-
-
+
+











	ident := fs.Arg(0)
	hashedPassword, err := cred.HashCredential(zid, ident, password)
	if err != nil {
		return 2, err
	}
	fmt.Printf("%v: %s\n%v: %s\n",
		meta.KeyCredential, hashedPassword,
		meta.KeyUserID, ident,
		api.KeyCredential, hashedPassword,
		api.KeyUserID, ident,
	)
	return 0, nil
}

func getPassword(prompt string) (string, error) {
	fmt.Fprintf(os.Stderr, "%s: ", prompt)
	password, err := term.ReadPassword(int(os.Stdin.Fd()))
	fmt.Fprintln(os.Stderr)
	return string(password), err
}

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
34
35
36
37
38
39
40
41
42
43
44






45
46
47
48
49
50
51
52


53
54
55

56
57
58


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




1

2
3

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


22
23
24

25

26
27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43




44
45
46
47
48
49



50
51
52
53

54
55
56


57



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

package cmd

import (
	"context"
	"flag"
	"net/http"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"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", defConfigfile, "configuration file")
	fs.String("c", "", "configuration file")
	fs.Uint("a", 0, "port number kernel service (0=disable)")
	fs.Uint("p", 23123, "port number web service")
	fs.String("d", "", "zettel directory")
	fs.Bool("r", false, "system-wide read-only mode")
	fs.Bool("v", false, "verbose mode")
	fs.Bool("debug", false, "debug mode")
}

func withDebug(fs *flag.FlagSet) bool {
	dbg := fs.Lookup("debug")
	return dbg != nil && dbg.Value.String() == "true"
}
func runFunc(*flag.FlagSet) (int, error) {
	var exitCode int
	err := kernel.Main.StartService(kernel.WebService)
	if err != nil {
		exitCode = 1
	}

func runFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) {
	exitCode, err := doRun(withDebug(fs))
	kernel.Main.WaitForShutdown()
	return exitCode, err
}

func doRun(debug bool) (int, error) {
func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) {
	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig)
	kern := kernel.Main
	kern.SetDebug(debug)
	if err := kern.StartService(kernel.WebService); err != nil {
	webLog := kern.GetLogger(kernel.WebService)
		return 1, err
	}
	return 0, nil

	var getUser getUserImpl
}

	logAuth := kern.GetLogger(kernel.AuthService)
func setupRouting(webSrv server.Server, placeManager place.Manager, authManager auth.Manager, rtConfig config.Config) {
	protectedPlaceManager, authPolicy := authManager.PlaceWithPolicy(webSrv, placeManager, rtConfig)
	api := api.New(webSrv, authManager, authManager, webSrv, rtConfig)
	wui := webui.New(webSrv, authManager, rtConfig, authManager, placeManager, authPolicy)

	ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, placeManager)
	ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedPlaceManager)
	ucGetMeta := usecase.NewGetMeta(protectedPlaceManager)
	ucGetZettel := usecase.NewGetZettel(protectedPlaceManager)
	logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser)
	ucGetUser := usecase.NewGetUser(authManager, boxManager)
	ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser)
	ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucQuery := usecase.NewQuery(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery)
	ucQuery.SetEvaluate(&ucEvaluate)
	ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery)
	ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery)
	ucListMeta := usecase.NewListMeta(protectedPlaceManager)
	ucListRoles := usecase.NewListRole(protectedPlaceManager)
	ucListTags := usecase.NewListTags(protectedPlaceManager)
	ucZettelContext := usecase.NewZettelContext(protectedPlaceManager)
	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager)
	ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager)
	ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager)
	ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	a := api.New(
		webLog.Clone().Str("adapter", "api").Child(),
		webSrv, authManager, authManager, rtConfig, authPolicy)
	wui := webui.New(
		webLog.Clone().Str("adapter", "wui").Child(),
		webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate)

	webSrv.Handle("/", wui.MakeGetRootHandler(protectedPlaceManager))
	webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler())
	webSrv.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler(
	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))
	}

		api.MakePostLoginHandlerAPI(ucAuthenticate),
		wui.MakePostLoginHandlerHTML(ucAuthenticate)))
	webSrv.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler())
	webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler())
	// Web user interface
	if !authManager.IsReadonly() {
		webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta))
		webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler(
			usecase.NewRenameZettel(protectedPlaceManager)))
		webSrv.AddZettelRoute('c', http.MethodGet, wui.MakeGetCopyZettelHandler(
			ucGetZettel, usecase.NewCopyZettel()))
		webSrv.AddZettelRoute('c', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
		webSrv.AddZettelRoute('d', http.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler(
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename))
		webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax))
		webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel))
		webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete))
			usecase.NewDeleteZettel(protectedPlaceManager)))
		webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(
		webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
			usecase.NewUpdateZettel(protectedPlaceManager)))
		webSrv.AddZettelRoute('f', http.MethodGet, wui.MakeGetFolgeZettelHandler(
			ucGetZettel, usecase.NewFolgeZettel(rtConfig)))
		webSrv.AddZettelRoute('f', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
		webSrv.AddZettelRoute('g', http.MethodGet, wui.MakeGetNewZettelHandler(
			ucGetZettel, usecase.NewNewZettel()))
		webSrv.AddZettelRoute('g', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
	}
	webSrv.AddListRoute('f', http.MethodGet, wui.MakeSearchHandler(
	webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
		usecase.NewSearch(protectedPlaceManager), ucGetMeta, ucGetZettel))
	webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler(
	webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
		ucListMeta, ucListRoles, ucListTags))
	webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler(
	webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel))
		ucParseZettel, ucGetMeta))
	webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(ucParseZettel, ucGetMeta))
	webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext))
	webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler())
	webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler(
		ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery))

	// API
	webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel))
	webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler(
	webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
		usecase.NewZettelOrder(protectedPlaceManager, ucParseZettel)))
	webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles))
	webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags))
	webSrv.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext))
	webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler(
	webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion))
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
		usecase.NewListMeta(protectedPlaceManager), ucGetMeta, ucParseZettel))
	webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler(
		ucParseZettel, ucGetMeta))
		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(placeManager))
		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
	}
}

type getUserImpl struct{}

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

Deleted cmd/cmd_run_simple.go.

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

package cmd

import (
	"flag"
	"fmt"
	"os"
	"strings"

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

func flgSimpleRun(fs *flag.FlagSet) {
	fs.String("d", "", "zettel directory")
}

func runSimpleFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) {
	kern := kernel.Main
	listenAddr := kern.GetConfig(kernel.WebService, kernel.WebListenAddress).(string)
	exitCode, err := doRun(false)
	if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
		kern.Log()
		kern.Log("--------------------------")
		kern.Log("Open your browser and enter the following URL:")
		kern.Log()
		kern.Log(fmt.Sprintf("    http://localhost%v", listenAddr[idx:]))
		kern.Log()
	}
	kern.WaitForShutdown()
	return exitCode, err
}

// 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 {
	dir := "./zettel"
	if err := os.MkdirAll(dir, 0750); err != nil {
		fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err)
		os.Exit(1)
	}
	return executeCommand("run-simple", "-d", dir)
}

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
24
25
26
27








28
29
30
31
32
33
34

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

package cmd

import (
	"flag"
	"sort"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/domain/meta"
	"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
	Places bool                // if true then places will be set up
	Header bool                // Print a heading on startup
	Flags  func(*flag.FlagSet) // function to set up flag.FlagSet
	flags  *flag.FlagSet       // flags that belong to the command
	Name       string              // command name as it appears on the command line
	Func       CommandFunc         // function that executes a command
	Simple     bool                // Operate in simple-mode
	Boxes      bool                // if true then boxes will be set up
	Header     bool                // Print a heading on startup
	LineServer bool                // Start admin line server
	SetFlags   func(*flag.FlagSet) // function to set up flag.FlagSet
	flags      *flag.FlagSet       // flags that belong to the command

}

// CommandFunc is the function that executes the command.
// It accepts the parsed command line parameters.
// It returns the exit code and an error.
type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error)
type CommandFunc func(*flag.FlagSet) (int, error)

// GetFlags return the flag.FlagSet defined for the command.
func (c *Command) GetFlags() *flag.FlagSet { return c.flags }

var commands = make(map[string]Command)

// RegisterCommand registers the given command.
func RegisterCommand(cmd Command) {
	if cmd.Name == "" || cmd.Func == nil {
		panic("Required command values missing")
	}
	if _, ok := commands[cmd.Name]; ok {
		panic("Command already registered: " + cmd.Name)
	}
	cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError)
	cmd.flags.String("l", logger.InfoLevel.String(), "log level specification")

	if cmd.Flags != nil {
		cmd.Flags(cmd.flags)
	if cmd.SetFlags != nil {
		cmd.SetFlags(cmd.flags)
	}
	commands[cmd.Name] = cmd
}

// Get returns the command identified by the given name and a bool to signal success.
func Get(name string) (Command, bool) {
	cmd, ok := commands[name]
	return cmd, ok
}

// List returns a sorted list of all registered command names.
func List() []string {
func List() []string { return maps.Keys(commands) }
	result := make([]string, 0, len(commands))
	for name := range commands {
		result = append(result, name)
	}
	sort.Strings(result)
	return result
}

Deleted cmd/fd_limit.go.

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















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

// +build !darwin

package cmd

func raiseFdLimit() error { return nil }

Deleted cmd/fd_limit_raise.go.

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

// +build darwin

package cmd

import (
	"log"
	"syscall"
)

const minFiles = 1048576

func raiseFdLimit() error {
	var rLimit syscall.Rlimit
	err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
	if err != nil {
		return err
	}
	if rLimit.Cur >= minFiles {
		return nil
	}
	rLimit.Cur = minFiles
	if rLimit.Cur > rLimit.Max {
		rLimit.Cur = rLimit.Max
	}
	err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
	if err != nil {
		return err
	}
	err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
	if err != nil {
		return err
	}
	if rLimit.Cur < minFiles {
		log.Printf("Make sure you have no more than %d files in all your places if you enabled notification\n", rLimit.Cur)
	}
	return nil
}

Changes to cmd/main.go.

1
2

3
4

5
6
7
8



9
10
11
12
13
14

15
16
17
18
19

20
21

22


23
24
25
26
27
28




29
30
31
32
33




34
35
36

37
38

39
40
41
42
43

44
45
46
47
48
49
50
51
52
53

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














1

2
3

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

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31




32
33
34
35
36




37
38
39
40
41
42

43


44

45
46
47

48
49
50
51
52
53
54
55
56
57

58
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

-
+

-
+




+
+
+





-
+





+


+

+
+


-
-
-
-
+
+
+
+

-
-
-
-
+
+
+
+


-
+
-
-
+
-



-
+









-
+



-
-
-
-
-
+
+
+
+
+
+


-
-
-
+
+
+
+

+
-
+
+
+
+




-
-
+
+








-
+
-

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



-
+


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



-
-
+
-

-
-
+
-







+
-
+
+
+
+
+






-
+


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



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

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

-
+


-
-
+
+

-
-
-
-
+
+
+
+
-

-
+




-
+


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

-
+


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

-
+
-
-
-

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













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


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



-
+



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

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

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

package cmd

import (
	"errors"
	"crypto/sha256"
	"flag"
	"fmt"
	"net"
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/impl"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/compbox"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"
	"zettelstore.de/z/place/progplace"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const (
const strRunSimple = "run-simple"
	defConfigfile = ".zscfg"
)


func init() {
	RegisterCommand(Command{
		Name: "help",
		Func: func(*flag.FlagSet, *meta.Meta) (int, error) {
		Func: func(*flag.FlagSet) (int, error) {
			fmt.Println("Available commands:")
			for _, name := range List() {
				fmt.Printf("- %q\n", name)
			}
			return 0, nil
		},
	})
	RegisterCommand(Command{
		Name:   "version",
		Func:   func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil },
		Func:   func(*flag.FlagSet) (int, error) { return 0, nil },
		Header: true,
	})
	RegisterCommand(Command{
		Name:   "run",
		Func:   runFunc,
		Places: true,
		Header: true,
		Flags:  flgRun,
		Name:       "run",
		Func:       runFunc,
		Boxes:      true,
		Header:     true,
		LineServer: true,
		SetFlags:   flgRun,
	})
	RegisterCommand(Command{
		Name:   "run-simple",
		Func:   runSimpleFunc,
		Places: true,
		Name:   strRunSimple,
		Func:   runFunc,
		Simple: true,
		Boxes:  true,
		Header: true,
		// LineServer: true,
		Flags:  flgSimpleRun,
		SetFlags: func(fs *flag.FlagSet) {
			// fs.Uint("a", 0, "port number kernel service (0=disable)")
			fs.String("d", "", "zettel directory")
		},
	})
	RegisterCommand(Command{
		Name: "file",
		Func: cmdFile,
		Flags: func(fs *flag.FlagSet) {
			fs.String("t", "html", "target output format")
		SetFlags: func(fs *flag.FlagSet) {
			fs.String("t", api.EncoderHTML.String(), "target output encoding")
		},
	})
	RegisterCommand(Command{
		Name: "password",
		Func: cmdPassword,
	})
}

func readConfig(fs *flag.FlagSet) (cfg *meta.Meta) {
func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) {
	var configFile string
	if configFlag := fs.Lookup("c"); configFlag != nil {
		configFile = configFlag.Value.String()
	} else {
		configFile = defConfigfile
	}
	content, err := os.ReadFile(configFile)
		if filename := configFlag.Value.String(); filename != "" {
			content, err := readConfiguration(filename)
			return filename, createConfiguration(content, err)
		}
	}
	filename, content, err := searchAndReadConfiguration()
	return filename, createConfiguration(content, err)
}

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

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

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

func getConfig(fs *flag.FlagSet) *meta.Meta {
	cfg := readConfig(fs)
func getConfig(fs *flag.FlagSet) (string, *meta.Meta) {
	filename, cfg := fetchStartupConfiguration(fs)
	fs.Visit(func(flg *flag.Flag) {
		switch flg.Name {
		case "p":
			if portStr, err := parsePort(flg.Value.String()); err == nil {
				cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr))
			cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String()))
			}
		case "a":
			if portStr, err := parsePort(flg.Value.String()); err == nil {
				cfg.Set(keyAdminPort, portStr)
			cfg.Set(keyAdminPort, flg.Value.String())
			}
		case "d":
			val := flg.Value.String()
			if strings.HasPrefix(val, "/") {
				val = "dir://" + val
			} else {
				val = "dir:" + val
			}
			deleteConfiguredBoxes(cfg)
			cfg.Set(keyPlaceOneURI, val)
			cfg.Set(keyBoxOneURI, val)
		case "l":
			cfg.Set(keyLogLevel, flg.Value.String())
		case "debug":
			cfg.Set(keyDebug, flg.Value.String())
		case "r":
			cfg.Set(keyReadOnly, flg.Value.String())
		case "v":
			cfg.Set(keyVerbose, flg.Value.String())
		}
	})
	return cfg
	return filename, cfg
}

func parsePort(s string) (string, error) {
	port, err := net.LookupPort("tcp", s)
	if err != nil {
func deleteConfiguredBoxes(cfg *meta.Meta) {
	for _, p := range cfg.PairsRest() {
		if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) {
		fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s)
		return "", err
	}
			cfg.Delete(key)
		}
	return strconv.Itoa(port), nil
	}
}

const (
	keyAdminPort           = "admin-port"
	keyDefaultDirPlaceType = "default-dir-place-type"
	keyInsecureCookie      = "insecure-cookie"
	keyListenAddr          = "listen-addr"
	keyOwner               = "owner"
	keyPersistentCookie    = "persistent-cookie"
	keyPlaceOneURI         = kernel.PlaceURIs + "1"
	keyReadOnly            = "read-only-mode"
	keyTokenLifetimeHTML   = "token-lifetime-html"
	keyTokenLifetimeAPI    = "token-lifetime-api"
	keyURLPrefix           = "url-prefix"
	keyVerbose             = "verbose"
	keyAdminPort         = "admin-port"
	keyAssetDir          = "asset-dir"
	keyBaseURL           = "base-url"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyInsecureHTML      = "insecure-html"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyReadOnly          = "read-only-mode"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) bool {
	debugMode := cfg.GetBool(keyDebug)
	if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel {
		kernel.Main.SetLogLevel(logger.DebugLevel.String())

func setServiceConfig(cfg *meta.Meta) error {
	ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
	}
	if logLevel, found := cfg.Get(keyLogLevel); found {
		kernel.Main.SetLogLevel(logLevel)
	}
	err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode)
	err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
	if val, found := cfg.Get(keyAdminPort); found {
		ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val)
		err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val)
	}

	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, ""))
	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly))
	err = setConfigValue(err, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, ""))
	err = setConfigValue(err, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly))

	ok = setConfigValue(
		ok, kernel.PlaceService, kernel.PlaceDefaultDirType,
		cfg.GetDefault(keyDefaultDirPlaceType, kernel.PlaceDirTypeNotify))
	ok = setConfigValue(ok, kernel.PlaceService, kernel.PlaceURIs+"1", "dir:./zettel")
	err = setConfigValue(
		err, kernel.BoxService, kernel.BoxDefaultDirType,
		cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify))
	err = setConfigValue(err, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel")
	format := kernel.PlaceURIs + "%v"
	for i := 1; ; i++ {
		key := fmt.Sprintf(format, i)
		key := kernel.BoxURIs + strconv.Itoa(i)
		val, found := cfg.Get(key)
		if !found {
			break
		}
		ok = setConfigValue(ok, kernel.PlaceService, key, val)
		err = setConfigValue(err, kernel.BoxService, key, val)
	}

	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebListenAddress,
		cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/"))
	ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))

	err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))

	err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	if val, found := cfg.Get(keyBaseURL); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val)
	}
	if val, found := cfg.Get(keyURLPrefix); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val)
	}
	err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))
	if val, found := cfg.Get(keyAssetDir); found {
	if !ok {
		return errors.New("unable to set configuration")
		err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val)
	}
	return nil
	return err == nil
}

func setConfigValue(ok bool, subsys kernel.Service, key string, val interface{}) bool {
	done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val))
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 !done {
		kernel.Main.Log("unable to set configuration:", key, val)
	}
	return ok && done
}

func setupOperations(cfg *meta.Meta, withPlaces bool) {
	var createManager kernel.CreatePlaceManagerFunc
	if withPlaces {
		err := raiseFdLimit()
		if err != nil {
			srvm := kernel.Main
			kernel.Main.GetKernelLogger().Error().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration")
			srvm.Log("Raising some limitions did not work:", err)
			srvm.Log("Prepare to encounter errors. Most of them can be mitigated. See the manual for details")
			srvm.SetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType, kernel.PlaceDirTypeSimple)
		}
		createManager = func(placeURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (place.Manager, error) {
			progplace.Setup(cfg)
			return manager.New(placeURIs, authManager, rtConfig)
		}
	}
	} else {
		createManager = func([]*url.URL, auth.Manager, config.Config) (place.Manager, error) { return nil, nil }
	}

	kernel.Main.SetCreators(
		func(readonly bool, owner id.Zid) (auth.Manager, error) {
			return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil
		},
		createManager,
		func(srv server.Server, plMgr place.Manager, authMgr auth.Manager, rtConfig config.Config) error {
			setupRouting(srv, plMgr, authMgr, rtConfig)
			return nil
	return err
		},
	)
}

func executeCommand(name string, args ...string) int {
	command, ok := Get(name)
	if !ok {
		fmt.Fprintf(os.Stderr, "Unknown command %q\n", name)
		return 1
	}
	fs := command.GetFlags()
	if err := fs.Parse(args); err != nil {
		fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err)
		return 1
	}
	cfg := getConfig(fs)
	if err := setServiceConfig(cfg); err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
	filename, cfg := getConfig(fs)
	if !setServiceConfig(cfg) {
		fs.Usage()
		return 2
	}

	kern := kernel.Main
	var createManager kernel.CreateBoxManagerFunc
	if command.Boxes {
		createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) {
			compbox.Setup(cfg)
			return manager.New(boxURIs, authManager, rtConfig)
		}
	} else {
		createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil }
	}

	secret := cfg.GetDefault("secret", "")
	if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" {
		fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret)
		return 2
	}
	cfg.Delete("secret")
	secret = fmt.Sprintf("%x", sha256.Sum256([]byte(secret)))
	setupOperations(cfg, command.Places)
	kernel.Main.Start(command.Header)
	exitCode, err := command.Func(fs, cfg)

	kern.SetCreators(
		func(readonly bool, owner id.Zid) (auth.Manager, error) {
			return impl.New(readonly, owner, secret), nil
		},
		createManager,
		func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error {
			setupRouting(srv, plMgr, authMgr, rtConfig)
			return nil
		},
	)

	if command.Simple {
		kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true")
	}
	kern.Start(command.Header, command.LineServer, filename)
	exitCode, err := command.Func(fs)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
	}
	kernel.Main.Shutdown(true)
	kern.Shutdown(true)
	return exitCode
}

// runSimple is called, when the user just starts the software via a double click
// or via a simple call “./zettelstore“ on the command line.
func runSimple() int {
	if _, _, err := searchAndReadConfiguration(); err == nil {
		return executeCommand(strRunSimple)
	}
	dir := "./zettel"
	if err := os.MkdirAll(dir, 0750); err != nil {
		fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err)
		return 1
	}
	return executeCommand(strRunSimple, "-d", dir)
}

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")

// Main is the real entrypoint of the zettelstore.
func Main(progName, buildVersion string) {
func Main(progName, buildVersion string) int {
	kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName)
	kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, buildVersion)
	var exitCode int
	if len(os.Args) <= 1 {
		exitCode = runSimple()
	} else {
		exitCode = executeCommand(os.Args[1], os.Args[2:]...)
	}
	if exitCode != 0 {
		os.Exit(exitCode)
	info := retrieveVCSInfo(buildVersion)
	fullVersion := info.revision
	if info.dirty {
		fullVersion += "-dirty"
	}
	kernel.Main.Setup(progName, fullVersion, info.time)
	flag.Parse()
	if *cpuprofile != "" || *memprofile != "" {
		if *cpuprofile != "" {
			kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile)
		} else {
			kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile)
		}
		defer kernel.Main.StopProfiling()
	}
	args := flag.Args()
	if len(args) == 0 {
		return runSimple()
	}
	return executeCommand(args[0], args[1:]...)
}

type vcsInfo struct {
	revision string
	dirty    bool
	time     time.Time
}

func retrieveVCSInfo(version string) vcsInfo {
	buildTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
	info, ok := debug.ReadBuildInfo()
	if !ok {
		return vcsInfo{revision: version, dirty: false, time: buildTime}
	}
	result := vcsInfo{time: buildTime}
	for _, kv := range info.Settings {
		switch kv.Key {
		case "vcs.revision":
			revision := "+" + kv.Value
			if len(revision) > 11 {
				revision = revision[:11]
}
			}
			result.revision = version + revision
		case "vcs.modified":
			if kv.Value == "true" {
				result.dirty = true
			}
		case "vcs.time":
			if t, err := time.Parse(time.RFC3339, kv.Value); err == nil {
				result.time = t
			}
		}
	}
	return result
}

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
1

2
3

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

// Package cmd 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/jsonenc"   // Allow to use JSON encoder.
	_ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder.
	_ "zettelstore.de/z/encoder/rawenc"    // Allow to use raw encoder.
	_ "zettelstore.de/z/encoder/mdenc"     // Allow to use markdown encoder.
	_ "zettelstore.de/z/encoder/shtmlenc"  // Allow to use SHTML encoder.
	_ "zettelstore.de/z/encoder/szenc"     // Allow to use Sz encoder.
	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
	_ "zettelstore.de/z/kernel/impl"       // Allow kernel implementation to create itself
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
	_ "zettelstore.de/z/place/constplace"  // Allow to use global internal place.
	_ "zettelstore.de/z/place/dirplace"    // Allow to use directory place.
	_ "zettelstore.de/z/place/fileplace"   // Allow to use file place.
	_ "zettelstore.de/z/place/memplace"    // Allow to use memory place.
	_ "zettelstore.de/z/place/progplace"   // Allow to use computed place.
)

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
1

2
3

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

20
21
22
23
24
25
26

27
28
29

-
+

-
+




+
+
+





+
+
+
-
+
+





-
+
+

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

// Package main is the starting point for the zettelstore command.
package main

import (
	"os"

import "zettelstore.de/z/cmd"
	"zettelstore.de/z/cmd"
)

// Version variable. Will be filled by build process.
var version string = ""

func main() {
	cmd.Main("Zettelstore", version)
	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

16
17
18
19
20
21


22
23
24
25
26
27

28
29
30


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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









-
+

-
+




+
+
+





-
-
+
-



-
-
+
+





-
+
-
-
-
+
+

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

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

// Package collect provides functions to collect items from a syntax tree.
package collect

import (
	"zettelstore.de/z/ast"
import "zettelstore.de/z/ast"
)

// Summary stores the relevant parts of the syntax tree
type Summary struct {
	Links  []*ast.Reference // list of all referenced links
	Images []*ast.Reference // list of all referenced images
	Links  []*ast.Reference // list of all linked material
	Embeds []*ast.Reference // list of all embedded material
	Cites  []*ast.CiteNode  // list of all referenced citations
}

// References returns all references mentioned in the given zettel. This also
// includes references to images.
func References(zn *ast.ZettelNode) Summary {
func References(zn *ast.ZettelNode) (s Summary) {
	lv := linkVisitor{}
	ast.NewTopDownTraverser(&lv).VisitBlockSlice(zn.Ast)
	return lv.summary
	ast.Walk(&s, &zn.Ast)
	return s
}

type linkVisitor struct {
	summary Summary
}

// VisitVerbatim does nothing.
func (lv *linkVisitor) VisitVerbatim(vn *ast.VerbatimNode) {}

// VisitRegion does nothing.
func (lv *linkVisitor) VisitRegion(rn *ast.RegionNode) {}

// VisitHeading does nothing.
func (lv *linkVisitor) VisitHeading(hn *ast.HeadingNode) {}

// VisitHRule does nothing.
func (lv *linkVisitor) VisitHRule(hn *ast.HRuleNode) {}

// VisitList does nothing.
func (lv *linkVisitor) VisitNestedList(ln *ast.NestedListNode) {}

// VisitDescriptionList does nothing.
func (lv *linkVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {}

// VisitPara does nothing.
func (lv *linkVisitor) VisitPara(pn *ast.ParaNode) {}

// VisitTable does nothing.
func (lv *linkVisitor) VisitTable(tn *ast.TableNode) {}

// VisitBLOB does nothing.
func (lv *linkVisitor) VisitBLOB(bn *ast.BLOBNode) {}

// VisitText does nothing.
func (lv *linkVisitor) VisitText(tn *ast.TextNode) {}

// VisitTag does nothing.
// Visit all node to collect data for the summary.
func (lv *linkVisitor) VisitTag(tn *ast.TagNode) {}

func (s *Summary) Visit(node ast.Node) ast.Visitor {
// VisitSpace does nothing.
func (lv *linkVisitor) VisitSpace(sn *ast.SpaceNode) {}

	switch n := node.(type) {
// VisitBreak does nothing.
func (lv *linkVisitor) VisitBreak(bn *ast.BreakNode) {}

	case *ast.TranscludeNode:
		s.Embeds = append(s.Embeds, n.Ref)
// VisitLink collects the given link as a reference.
func (lv *linkVisitor) VisitLink(ln *ast.LinkNode) {
	lv.summary.Links = append(lv.summary.Links, ln.Ref)
	case *ast.LinkNode:
		s.Links = append(s.Links, n.Ref)
}

	case *ast.EmbedRefNode:
// VisitImage collects the image links as a reference.
func (lv *linkVisitor) VisitImage(in *ast.ImageNode) {
	if in.Ref != nil {
		lv.summary.Images = append(lv.summary.Images, in.Ref)
	}
}

		s.Embeds = append(s.Embeds, n.Ref)
// VisitCite collects the citation.
func (lv *linkVisitor) VisitCite(cn *ast.CiteNode) {
	lv.summary.Cites = append(lv.summary.Cites, cn)
}

	case *ast.CiteNode:
		s.Cites = append(s.Cites, n)
	}
	return s
// VisitFootnote does nothing.
func (lv *linkVisitor) VisitFootnote(fn *ast.FootnoteNode) {}

}
// VisitMark does nothing.
func (lv *linkVisitor) VisitMark(mn *ast.MarkNode) {}

// VisitFormat does nothing.
func (lv *linkVisitor) VisitFormat(fn *ast.FormatNode) {}

// VisitLiteral does nothing.
func (lv *linkVisitor) VisitLiteral(ln *ast.LiteralNode) {}

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

// Package collect_test provides some unit test for collectors.
package collect_test

import (
	"testing"
23
24
25
26
27
28
29

30
31
32
33


34
35
36
37
38
39
40

41
42
43
44
45
46


47
48
49
50
51
52
53
54
55
56


57
58

59
60
61
62
63
64
65
66
67
68


69
70
26
27
28
29
30
31
32
33
34
35


36
37
38
39
40




41


42
43


44
45
46
47
48
49
50
51
52
53
54

55
56
57

58






59
60


61
62
63
64







+


-
-
+
+



-
-
-
-
+
-
-


-
-
+
+









-
+
+

-
+
-
-
-
-
-
-


-
-
+
+


	if !r.IsValid() {
		panic(s)
	}
	return r
}

func TestLinks(t *testing.T) {
	t.Parallel()
	zn := &ast.ZettelNode{}
	summary := collect.References(zn)
	if summary.Links != nil || summary.Images != nil {
		t.Error("No links/images expected, but got:", summary.Links, "and", summary.Images)
	if summary.Links != nil || summary.Embeds != nil {
		t.Error("No links/images expected, but got:", summary.Links, "and", summary.Embeds)
	}

	intNode := &ast.LinkNode{Ref: parseRef("01234567890123")}
	para := &ast.ParaNode{
		Inlines: ast.InlineSlice{
			intNode,
			&ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")},
	para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")})
		},
	}
	zn.Ast = ast.BlockSlice{para}
	summary = collect.References(zn)
	if summary.Links == nil || summary.Images != nil {
		t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Images)
	if summary.Links == nil || summary.Embeds != nil {
		t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Embeds)
	}

	para.Inlines = append(para.Inlines, intNode)
	summary = collect.References(zn)
	if cnt := len(summary.Links); cnt != 3 {
		t.Error("Link count does not work. Expected: 3, got", summary.Links)
	}
}

func TestImage(t *testing.T) {
func TestEmbed(t *testing.T) {
	t.Parallel()
	zn := &ast.ZettelNode{
		Ast: ast.BlockSlice{
		Ast: ast.BlockSlice{ast.CreateParaNode(&ast.EmbedRefNode{Ref: parseRef("12345678901234")})},
			&ast.ParaNode{
				Inlines: ast.InlineSlice{
					&ast.ImageNode{Ref: parseRef("12345678901234")},
				},
			},
		},
	}
	summary := collect.References(zn)
	if summary.Images == nil {
		t.Error("Only image expected, but got: ", summary.Images)
	if summary.Embeds == nil {
		t.Error("Only image expected, but got: ", summary.Embeds)
	}
}

Changes to collect/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
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

-
+

-
+




+
+
+










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


















-
-
+
+






-
+
+
+







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

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

// Order of internal reference within the given zettel.
func Order(zn *ast.ZettelNode) (result []*ast.Reference) {
	for _, bn := range zn.Ast {
		if ln, ok := bn.(*ast.NestedListNode); ok {
			switch ln.Code {
			case ast.NestedListOrdered, ast.NestedListUnordered:
				for _, is := range ln.Items {
					if ref := firstItemZettelReference(is); ref != nil {
						result = append(result, ref)
		ln, ok := bn.(*ast.NestedListNode)
		if !ok {
			continue
		}
		switch ln.Kind {
		case ast.NestedListOrdered, ast.NestedListUnordered:
			for _, is := range ln.Items {
				if ref := firstItemZettelReference(is); ref != nil {
					result = append(result, ref)
					}
				}
			}
		}
	}
	return result
}

func firstItemZettelReference(is ast.ItemSlice) *ast.Reference {
	for _, in := range is {
		if pn, ok := in.(*ast.ParaNode); ok {
			if ref := firstInlineZettelReference(pn.Inlines); ref != nil {
				return ref
			}
		}
	}
	return nil
}

func firstInlineZettelReference(ins ast.InlineSlice) (result *ast.Reference) {
	for _, inl := range ins {
func firstInlineZettelReference(is ast.InlineSlice) (result *ast.Reference) {
	for _, inl := range is {
		switch in := inl.(type) {
		case *ast.LinkNode:
			if ref := in.Ref; ref.IsZettel() {
				return ref
			}
			result = firstInlineZettelReference(in.Inlines)
		case *ast.ImageNode:
		case *ast.EmbedRefNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.EmbedBLOBNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:

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















































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

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

// 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(map[string]bool)
	mapLocal := make(map[string]bool)
	mapExternal := make(map[string]bool)
	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 map[string]bool, ref *ast.Reference) []*ast.Reference {
	s := ref.String()
	if _, ok := refSet[s]; !ok {
		reflist = append(reflist, ref)
		refSet[s] = true
	}
	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
33
34

35
36

37
38
39
40
41
42


43
44
45


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

-
+

-
+




+
+
+






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






-
-
-
+
-
-
-
+
-
-
+

-
-
-
+
-
-
+




-
-
+
+

-
-
+
+






-
-
-
-
-
-
-
-
-
-
-




+
+
+
-
+






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

-
+


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

-
+

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

// Package config provides functions to retrieve runtime configuration data.
package config

import (
	"context"

	"zettelstore.de/z/domain/id"
	"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"
	KeyShowBackLinks        = "show-back-links"
	KeyShowFolgeLinks       = "show-folge-links"
	KeyShowSubordinateLinks = "show-subordinate-links"
	KeyShowSuccessorLinks   = "show-successor-links"
	// api.KeyLang
)

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig

	// AddDefaultValues enriches the given meta data with its default values.
	AddDefaultValues(m *meta.Meta) *meta.Meta

	// Get returns the value of the given key. It searches first in the given metadata,
	// GetDefaultTitle returns the current value of the "default-title" key.
	GetDefaultTitle() string

	// then in the data of the current user, and at last in the system-wide data.
	// GetDefaultRole returns the current value of the "default-role" key.
	GetDefaultRole() string
	Get(ctx context.Context, m *meta.Meta, key string) string

	// GetDefaultSyntax returns the current value of the "default-syntax" key.
	GetDefaultSyntax() string

	// AddDefaultValues enriches the given meta data with its default values.
	// GetDefaultLang returns the current value of the "default-lang" key.
	GetDefaultLang() string
	AddDefaultValues(context.Context, *meta.Meta) *meta.Meta

	// GetSiteName returns the current value of the "site-name" key.
	GetSiteName() string

	// GetHomeZettel returns the value of the "home-zettel" key.
	GetHomeZettel() id.Zid
	// GetHTMLInsecurity returns the current
	GetHTMLInsecurity() HTMLInsecurity

	// GetDefaultVisibility returns the default value for zettel visibility.
	GetDefaultVisibility() meta.Visibility
	// GetMaxTransclusions returns the maximum number of indirect transclusions.
	GetMaxTransclusions() int

	// GetYAMLHeader returns the current value of the "yaml-header" key.
	GetYAMLHeader() bool

	// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
	GetZettelFileSyntax() []string

	// GetMarkerExternal returns the current value of the "marker-external" key.
	GetMarkerExternal() string

	// GetFooterHTML returns HTML code that should be embedded into the footer
	// of each WebUI page.
	GetFooterHTML() string

	// GetListPageSize returns the maximum length of a list to be returned in WebUI.
	// A value less or equal to zero signals no limit.
	GetListPageSize() int
}

// AuthConfig are relevant configuration values for authentication.
type AuthConfig interface {
	// GetSimpleMode returns true if system tuns in simple-mode.
	GetSimpleMode() bool

	// GetExpertMode returns the current value of the "expert-mode" key
	// GetExpertMode returns the current value of the "expert-mode" key.
	GetExpertMode() bool

	// GetVisibility returns the visibility value of the metadata.
	GetVisibility(m *meta.Meta) meta.Visibility
}

// GetTitle returns the value of the "title" key of the given meta. If there
// is no such value, GetDefaultTitle is returned.
// HTMLInsecurity states what kind of insecure HTML is allowed.
// The lowest value is the most secure one (disallowing any HTML)
func GetTitle(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeyTitle); ok {
		return val
	}
	return cfg.GetDefaultTitle()
type HTMLInsecurity uint8

// Constant values for HTMLInsecurity:
const (
	NoHTML HTMLInsecurity = iota
}

	SyntaxHTML
	MarkdownHTML
	ZettelmarkupHTML
)
// GetRole returns the value of the "role" key of the given meta. If there
// is no such value, GetDefaultRole is returned.
func GetRole(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeyRole); ok {
		return val
	}
	return cfg.GetDefaultRole()

func (hi HTMLInsecurity) String() string {
	switch hi {
	case SyntaxHTML:
		return "html"
}

	case MarkdownHTML:
// GetSyntax returns the value of the "syntax" key of the given meta. If there
// is no such value, GetDefaultSyntax is returned.
		return "markdown"
func GetSyntax(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeySyntax); ok {
		return val
	case ZettelmarkupHTML:
		return "zettelmarkup"
	}
	return cfg.GetDefaultSyntax()
	return "secure"
}

// GetLang returns the value of the "lang" key of the given meta. If there is
// no such value, GetDefaultLang is returned.
func GetLang(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeyLang); ok {
		return val
// AllowHTML returns true, if the given HTML insecurity level matches the given syntax value.
func (hi HTMLInsecurity) AllowHTML(syntax string) bool {
	switch hi {
	case SyntaxHTML:
		return syntax == meta.SyntaxHTML
	case MarkdownHTML:
		return syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD
	case ZettelmarkupHTML:
		return syntax == meta.SyntaxZmk || syntax == meta.SyntaxHTML ||
			syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD
	}
	return cfg.GetDefaultLang()
	return false
}

Added docs/development/00010000000000.zettel.












1
2
3
4
5
6
7
8
9
10
11
+
+
+
+
+
+
+
+
+
+
+
id: 00010000000000
title: Developments Notes
role: zettel
syntax: zmk
created: 00010101000000
modified: 20231218182020

* [[Required Software|20210916193200]]
* [[Fuzzing tests|20221026184300]]
* [[Checklist for Release|20210916194900]]
* [[Development tools|20231218181900]]

Added 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
23
24
25
26
27
28
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 20210916193200
title: Required Software
role: zettel
syntax: zmk
created: 20210916193200
modified: 20231213194509

The following software must be installed:

* A current, supported [[release of Go|https://go.dev/doc/devel/release]],
* [[Fossil|https://fossil-scm.org/]],
* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only).

Make sure that the software is in your path, e.g. via:
```sh
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin
```

The internal build tool need the following software.
It can be installed / updated via the build tool itself: ``go run tools/devtools/devtools.go``.

Otherwise you can install the software by hand:

* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``,
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``,
* [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``,

Added 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
59
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk
created: 20210916194900
modified: 20231213194631

# Sync with the official repository
#* ``fossil sync -u``
# Make sure that there is no workspace defined.
#* ``ls ..`` must not have a file ''go.work'', in no parent folder.
# Make sure that all dependencies are up-to-date.
#* ``cat go.mod``
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``).
# All internal tests must succeed:
#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``).
# The API tests must succeed on every development platform:
#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``).
# Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual:
#* ``go run -race cmd/zettelstore/main.go run -d docs/manual``
#* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt``
#* Check all ""Error: 404 Not Found""
#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''.
#* Try to resolve other error messages and warnings
#* Warnings about empty content can be ignored
# 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``.
# Create a development 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
# Set file ''VERSION'' to the new release version.
  It _must_ consist of three digits: 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''.
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``).
# Create the release:
#* ``go run tools/build/build.go release`` (alternatively: ``make release``).
# Remove previous executables:
#* ``fossil uv remove --glob '*-PREVVERSION*'``
# Add executables for release:
#* ``cd releases``
#* ``fossil uv add *.zip``
#* ``cd ..``
#* Synchronize with main repository:
#* ``fossil sync -u``
# Enable autosync:
#* ``fossil setting autosync on``

Added docs/development/20221026184300.zettel.















1
2
3
4
5
6
7
8
9
10
11
12
13
14
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 20221026184300
title: Fuzzing Tests
role: zettel
syntax: zmk
created: 20221026184320
modified: 20221102140156

The source code contains some simple [[fuzzing tests|https://go.dev/security/fuzz/]].
You should call them regularly to make sure that the software will cope with unusual input.

```sh
go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/draw
go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/zettelmark
```

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
117
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 20231218181900
title: Development tools
role: zettel
syntax: zmk
created: 20231218181956
modified: 20231218184500

The source code contains some tools to assist the development of Zettelstore.
These are located in the ''tools'' directory.

Most tool support the generic option ``-v``, which log internal activities.

Some of the tools can be called easier by using ``make``, that reads in a provided ''Makefile''.

=== Check
The ""check"" tool automates some testing activities.
It is called via the command line:
```
# go run tools/check/check.go
```
There is an additional option ``-r`` to check in advance of a release.

The following checks are executed:
* Execution of unit tests, like ``go test ./...``
* Analyze the source code for general problems, as in ``go vet ./...``
* Tries to find shadowed variable, via ``shadow ./...``
* Performs some additional checks on the source code, via ``staticcheck ./...``
* Checks the usage of function parameters and usage of return values, via ``unparam ./...``.
  In case the option ''-r'' is set, the check includes exported functions and internal tests.
* In case option ''-r'' is set, the source code is checked against the vulnerability database, via ``govulncheck ./...``

Please note, that most of the tools above are not automatically installed in a standard Go distribution.
Use the command ""devtools"" to install them.

=== Devtools
The following command installs all needed tools:
```
# go run tooles/devtools/devtools.go
```
It will also automatically update these tools.

=== TestAPI
The following command will perform some high-level tests:
```sh
# go run tools/testapi/testapi.go
```
Basically, a Zettelstore will be started and then API calls will be made to simulate some typical activities with the Zettelstore.

If a Zettelstore is already running on port 23123, this Zettelstore will be used instead.
Even if the API test should clean up later, some zettel might stay created if a test fails.
This feature is used, if you want to have more control on the running Zettelstore.
You should start it with the following command:
```sh
# go run -race cmd/zettelstore/main.go run -c testdata/testbox/19700101000000.zettel
```
This allows you to debug failing API tests.

=== HTMLlint
The following command will check the generated HTML code for validity:
```sh
# go run tools/htmllint/htmllint.go
```
In addition, you might specify the URL od a running Zettelstore.
Otherwise ''http://localhost:23123'' is used.

This command fetches first the list of all zettel.
This list is used to check the generated HTML code (''ZID'' is the paceholder for the zettel identification):

* Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel''
* Check all zettel web views, via the path ''/h/ZID''
* The info page of all zettel is checked, via path ''/i/ZID''
* A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID''
* 10 random zettel are checked for a valid create form, via ''/c/ZID''
* The zettel rename form will be checked for 100 zettel, via ''/b/ZID''
* A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID''

Depending on the selected Zettelstore, the command might take a long time.

You can shorten the time, if you disable any zettel query in the footer.

=== Build
The ""build"" tool allows to build the software, either for tests or for a release.

The following command will create a Zettelstore executable for the architecture of the current computer:
```sh
# go tools/build/build.go build
```
You will find the executable in the ''bin'' directory.

A full release will be build in the directory ''releases'', containing ZIP files for the computer architectures ""Linux/amd64"", ""Linux/arm"", ""MacOS/arm64"", ""MacOS/amd64"", and ""Windows/amd64"".
In addition, the manual is also build as a ZIP file:
```sh
# go run tools/build/build.go release
```

If you just want the ZIP file with the manual, please use:
```sh
# go run tools/build/build.go manual
```

In case you want to check the version of the Zettelstore to be build, use:
```sh
# go run tools/build/build.go version
```

=== Clean
To remove the directories ''bin'' and ''releases'', as well as all cached Go libraries used by Zettelstore, execute:
```sh
# go run tools/clean/clean.go
```

Internally, the following commands are executed
```sh
# rm -rf bin releases
# go clean ./...
# go clean -cache -modcache -testcache
```

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
14




+
-
+


-
+

-
+



id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
created: 00010101000000
default-copyright: (c) 2020-2021 by Detlef Stern <ds@zettelstore.de>
default-copyright: (c) 2020-present by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-html: <hr><p><a href="/home/doc/trunk/www/impri.wiki">Imprint / Privacy</a></p>
footer-zettel: 00001000000100
home-zettel: 00001000000000
no-index: true
modified: 20221205173642
site-name: Zettelstore Manual
visibility: owner

Added docs/manual/00000000025001.








1
2
3
4
5
6
7
+
+
+
+
+
+
+
id: 00000000025001
title: Zettelstore User CSS
role: configuration
syntax: css
created: 20210622110143
modified: 20220926183101
visibility: public

Added docs/manual/00000000025001.css.



1
2
+
+
/* User-defined CSS */
.example { border-style: dotted !important }

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
1
2
3
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: 20210301190630
modified: 20231125185455
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
* [[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/00001000000100.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
id: 00001000000100
title: Footer Zettel
role: configuration
syntax: zmk
created: 20221205173520
modified: 20221207175927

[[Imprint / Privacy|/home/doc/trunk/www/impri.wiki]]

Changes to docs/manual/00001002000000.zettel.

1
2

3
4
5


6
7
8
9
10
11




12
13
14
15

16
17
18

19
20
21

22
23
24
25
26
27

28
29
30
31






1
2
3
4
5

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

21
22
23

24
25
26

27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42
43


+


-
+
+






+
+
+
+



-
+


-
+


-
+





-
+




+
+
+
+
+
+
id: 00001002000000
title: Design goals for the Zettelstore
role: manual
tags: #design #goal #manual #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20230624171152

Zettelstore supports the following design goals:

; Longevity of stored notes / zettel
: Every zettel you create should be readable without the help of any tool, even without Zettelstore.
: It should be not hard to write other software that works with your zettel.
: Normal zettel should be stored in a single file.
  If this is not possible: at most in two files: one for the metadata, one for the content.
  The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable.
: There is no additional database.
; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.
  If your device is securely configured, there should be no risk that others are able to read or update your zettel.
  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 place and start working.
: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working.
: Upgrading the software is done just by replacing the executable with a newer one.
; Ease of operation
: There is only one executable for Zettelstore and one directory, where your zettel are placed.
: There is only one executable for Zettelstore and one directory, where your zettel are stored.
: If you decide to use multiple directories, you are free to configure Zettelstore appropriately.
; Multiple modes of operation
: You can use Zettelstore as a standalone software on your device, but you are not restricted to it.
: You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel.
; Multiple user interfaces
: Zettelstore provides a default web-based user interface.
: Zettelstore provides a default [[web-based user interface|00001014000000]].
  Anybody can provide alternative user interfaces, e.g. for special purposes.
; Simple service
: The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them.
: External software can be written to deeply analyze your zettel and the structures they form.
; Security by default
: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks.
: If you know what use are doing, Zettelstore allows you to relax some security-related preferences.
  However, even in this case, the more secure way is chosen.
: The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed.
: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software.

Changes to docs/manual/00001003000000.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
1
2
3
4
5
6
7
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: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
modified: 20220119145756

=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click
* A sub-directory ""zettel"" will be created in the directory where you placed the executable.
* 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.]
* 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 and learn about the various ways to write zettel.
* 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.

=== The intermediate user
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
* ...
Please follow [[these instructions|00001003300000]].

=== The server administrator
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
# sudo mv zettelstore /usr/local/bin/zettelstore
```
Create a group named ''zettelstore'':
```sh
# sudo groupadd --system zettelstore
```
Create a system user of that group, named ''zettelstore'', with a home folder:
```sh
# sudo useradd --system --gid zettelstore \
    --create-home --home-dir /var/lib/zettelstore \
    --shell /usr/sbin/nologin \
    --comment "Zettelstore server" \
    zettelstore
```
Create a systemd service file and place it into ''/etc/systemd/system/zettelstore.service'':
```ini
[Unit]
Description=Zettelstore
After=network.target

Please follow [[these instructions|00001003600000]].
[Service]
Type=simple
User=zettelstore
Group=zettelstore
ExecStart=/usr/local/bin/zettelstore run -d /var/lib/zettelstore
WorkingDirectory=/var/lib/zettelstore

[Install]
WantedBy=multi-user.target
```
Double-check everything. Now you can enable and start the zettelstore as a service:
```sh
# sudo systemctl daemon-reload
# sudo systemctl enable zettelstore
# sudo systemctl start zettelstore
```
Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.:
```sh
# sudo systemctl status zettelstore  # verify that it is running
# sudo journalctl -u zettelstore     # obtain the output of the running zettelstore
```

Added docs/manual/00001003300000.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: 00001003300000
title: Zettelstore installation for the intermediate user
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
modified: 20220114175754

You already tried the Zettelstore software and now you want to use it permanently.
Zettelstore should start automatically when you log into your computer.

* Grab the appropriate executable and copy it into the appropriate directory
* If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]].
* If you created a startup configuration file, you need to test it:
** Start a command line prompt for your operating system.
** Navigate to the directory, where you placed the Zettelstore executable.
   In most cases, this is done by the command ``cd DIR``, where ''DIR'' denotes the directory, where you placed the executable.
** Start the Zettelstore:
*** On Windows execute the command ``zettelstore.exe run -c CONFIG_FILE``
*** On macOS execute the command ``./zettelstore run -c CONFIG_FILE``
*** On Linux execute the command ``./zettelstore run -c CONFIG_FILE``
** In all cases ''CONFIG_FILE'' must be substituted by file name where you wrote the startup configuration.
** If you encounter some error messages, update the startup configuration, and try again.
* Depending on your operating system, there are different ways to register Zettelstore to start automatically:
** [[Windows|00001003305000]]
** [[macOS|00001003310000]]
** [[Linux|00001003315000]]

Added docs/manual/00001003305000.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001003305000
title: Enable Zettelstore to start automatically on Windows
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
modified: 20220218125541

Windows is a complicated beast. There are several ways to automatically start Zettelstore.

=== Startup folder

One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd]].
Open the folder where you have placed in the Explorer.
Create a shortcut file for the Zettelstore executable.
There are some ways to do this:
* Execute a right-click on the executable, and choose the menu entry ""Create shortcut"",
* Execute a right-click on the executable, and then click Send To > Desktop (Create shortcut).
* Drag the executable to your Desktop with pressing the ''Alt''-Key.

If you have created the shortcut file, you must move it into the Startup folder.
Press the Windows logo key and the key ''R'', type ''shell:startup''.
Select the OK button.
This will open the Startup folder.
Move the shortcut file into this folder.

The next time you log into your computer, Zettelstore will be started automatically.
However, it remains visible, at least in the task bar.

You can modify the behavior by changing some properties of the shortcut file.

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

To start the Task scheduler management console, press the Windows logo key and the key ''R'', type ''taskschd.msc''.
Select the OK button.

{{00001003305102}}

This will start the ""Task Scheduler"".

Now, create a new task with ""Create Task ...""

{{00001003305104}}

Enter a name for the task, e.g. ""Zettelstore"" and select the options ""Run whether user is logged in or not"" and ""Do not store password.""

{{00001003305106}}

Create a new trigger.

{{00001003305108}}

Select the option ""At startup"".

{{00001003305110}}

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

If you press the OK button, the ""Create Task"" tab shows up as on the right image.

{{00001003305114}}\ {{00001003305116}}

If you have created a startup configuration file, you must enter something into the field ""Add arguments (optional)"".
Unfortunately, the text box is too narrow to fully see its content.

I have entered the string ''run -c "C:\\Users\\Detlef Stern\\bin\\zsconfig.txt"'', because my startup configuration file has the name ''zsconfig.txt'' and I placed it into the same folder that also contains the Zettelstore executable.
Maybe you have to adapt to this.

You must also enter appropriate data for the other form fields.
If you press the OK button, the ""Create Task"" tab shows up as on the right image.

{{00001003305118}}\ {{00001003305120}}

You should disable any additional conditions, since you typically want to use Zettelstore unconditionally.
Especially, make sure that ""Start the task only if the computer is on AC power"" is disabled.
Otherwise Zettelstore will not start if you run on battery power.

{{00001003305122}}

On the ""Settings"" tab, you should disable the option ""Stop the task if it runs longer than:"".

{{00001003305124}}

After entering the data, press the OK button.
Under some circumstances, Windows asks for permission and you have to enter your password.

As the last step, you could run the freshly created task manually.

Open your browser, enter the appropriate URL and use your Zettelstore.
In case of errors, the task will most likely stop immediately.
Make sure that all data you have entered is valid.
To not forget to check the content of the startup configuration file.
Use the command prompt to debug your configuration.

Sometimes, for example when your computer was in stand-by and it wakes up, these tasks are not started.
In this case execute the task scheduler and run the task manually.

Added docs/manual/00001003305102.png.

cannot compute difference between binary files

Added docs/manual/00001003305104.png.

cannot compute difference between binary files

Added docs/manual/00001003305106.png.

cannot compute difference between binary files

Added docs/manual/00001003305108.png.

cannot compute difference between binary files

Added docs/manual/00001003305110.png.

cannot compute difference between binary files

Added docs/manual/00001003305112.png.

cannot compute difference between binary files

Added docs/manual/00001003305114.png.

cannot compute difference between binary files

Added docs/manual/00001003305116.png.

cannot compute difference between binary files

Added docs/manual/00001003305118.png.

cannot compute difference between binary files

Added docs/manual/00001003305120.png.

cannot compute difference between binary files

Added docs/manual/00001003305122.png.

cannot compute difference between binary files

Added docs/manual/00001003305124.png.

cannot compute difference between binary files

Added docs/manual/00001003310000.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001003310000
title: Enable Zettelstore to start automatically on macOS
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
modified: 20220119124635

There are several ways to automatically start Zettelstore.

* [[Login Items|#login-items]]
* [[Launch Agent|#launch-agent]]

=== Login Items

Via macOS's system preferences, everybody is able to specify executables that are started when a user is logged in.
To do this, start system preferences and select ""Users & Groups"".

{{00001003310104}}

In the next screen, select the current user and then click on ""Login Items"".

{{00001003310106}}

Click on the plus sign at the bottom and select the Zettelstore executable.

{{00001003310108}}

Optionally select the ""Hide"" check box.

{{00001003310110}}

The next time you log into your macOS computer, Zettelstore will be started automatically.

Unfortunately, hiding the Zettelstore windows does not always work.
Therefore, this method is just a way to automate navigating to the directory where the Zettelstore executable is placed and to click on that icon.

If you don't want the Zettelstore window, you should try the next method.

=== Launch Agent

If you want to execute Zettelstore automatically and less visible, and if you know a little bit about working in the terminal application, then you could try to run Zettelstore under the control of the [[Launchd system|https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html]].

First, you have to create a description for ""Launchd"".
This is a text file named ''zettelstore.plist'' with the following content.
It assumes that you have copied the Zettelstore executable in a local folder called ''~/bin'' and have created a file for [[startup configuration|00001004010000]] called ''zettelstore.cfg'', which is placed in the same folder[^If you are not using a configuration file, just remove the lines ``<string>-c</string>`` and ``<string>/Users/USERNAME/bin/zettelstore.cfg</string>``.]:

```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>Label</key>
		<string>de.zettelstore</string>

		<key>ProgramArguments</key>
		<array>
			<string>/Users/USERNAME/bin/zettelstore</string>
			<string>run</string>
			<string>-c</string>
			<string>/Users/USERNAME/bin/zettelstore.cfg</string>
		</array>

		<key>WorkingDirectory</key>
		<string>/Users/USERNAME</string>

		<key>EnvironmentVariables</key>
		<dict>
			<key>HOME</key>
			<string>/Users/USERNAME</string>
		</dict>

		<key>KeepAlive</key>
		<true/>

		<key>LowPriorityIO</key>
		<true/>

		<key>ProcessType</key>
		<string>Background</string>

		<key>StandardOutPath</key>
		<string>/Users/USERNAME/Library/Logs/Zettelstore.log</string>

		<key>StandardErrorPath</key>
		<string>/Users/USERNAME/Library/Logs/Zettelstore-Errors.log</string>
	</dict>
</plist>
```

You must substitute all occurrences of ''USERNAME'' with your user name.

Place this file into the user specific folder ''~/Library/LaunchAgents''.

Log out and in again, or execute the command ``launchctl load ~/Library/LaunchAgents/zettelstore.plist``.

Added docs/manual/00001003310104.png.

cannot compute difference between binary files

Added docs/manual/00001003310106.png.

cannot compute difference between binary files

Added docs/manual/00001003310108.png.

cannot compute difference between binary files

Added docs/manual/00001003310110.png.

cannot compute difference between binary files

Added docs/manual/00001003315000.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: 00001003315000
title: Enable Zettelstore to start automatically on Linux
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
modified: 20220307104944

Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore.

* One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]].
** See below for a lighter alternative.
* If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool"").
  It allows to specify application that should run on startup / login.
* [[KDE|https://kde.org/]] provides a system setting to [[autostart|https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/autostart/]] applications.
* [[Xfce|https://xfce.org/]] allows to specify [[autostart applications|https://docs.xfce.org/xfce/xfce4-session/preferences#application_autostart]].
* [[LXDE|https://www.lxde.org/]] uses [[LXSession Edit|https://wiki.lxde.org/en/LXSession_Edit]] to allow users to specify autostart applications.

If you use a different desktop environment, it often helps to to provide its name and the string ""autostart"" to google for it with the search engine of your choice.

Yet another way is to make use of the middleware that is provided.
Many Linux distributions make use of [[systemd|https://systemd.io/]], which allows to start processes on behalf of an user.
On the command line, adapt the following script to your own needs and execute it:
```
# mkdir -p "$HOME/.config/systemd/user"
# cd "$HOME/.config/systemd/user"
# cat <<__EOF__ > zettelstore.service
[Unit]
Description=Zettelstore
After=network.target home.mount

[Service]
ExecStart=/usr/local/bin/zettelstore run -d zettel

[Install]
WantedBy=default.target
__EOF__
# systemctl --user daemon-reload
# systemctl --user enable zettelstore.service
# systemctl --user start zettelstore.service
# systemctl --user status zettelstore.service
```
The last command should output some lines to indicate success.

Added docs/manual/00001003600000.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001003600000
title: Installation of Zettelstore on a server
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
modified: 20211125185833

You want to provide a shared Zettelstore that can be used from your various devices.
Installing Zettelstore as a Linux service is not that hard.

Grab the appropriate executable and copy it into the appropriate directory:
```sh
# sudo mv zettelstore /usr/local/bin/zettelstore
```
Create a group named ''zettelstore'':
```sh
# sudo groupadd --system zettelstore
```
Create a system user of that group, named ''zettelstore'', with a home folder:
```sh
# sudo useradd --system --gid zettelstore \
    --create-home --home-dir /var/lib/zettelstore \
    --shell /usr/sbin/nologin \
    --comment "Zettelstore server" \
    zettelstore
```
Create a systemd service file and store it into ''/etc/systemd/system/zettelstore.service'':
```ini
[Unit]
Description=Zettelstore
After=network.target

[Service]
Type=simple
User=zettelstore
Group=zettelstore
ExecStart=/usr/local/bin/zettelstore run -d /var/lib/zettelstore
WorkingDirectory=/var/lib/zettelstore

[Install]
WantedBy=multi-user.target
```
Double-check everything. Now you can enable and start the zettelstore as a service:
```sh
# sudo systemctl daemon-reload
# sudo systemctl enable zettelstore
# sudo systemctl start zettelstore
```
Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.:
```sh
# sudo systemctl status zettelstore  # verify that it is running
# sudo journalctl -u zettelstore     # obtain the output of the running zettelstore
```

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

1
2
3
4
5
6

7
8
9
10

11
12
13
14
15
16
17
18
19

20
21

22
23
24

25
26




27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64



65
66
67
68
69




70
71
72
73
74
75
76
77
78
79
80
81
82
83
84


85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109


110
111
112
113
114
115

116
117
118

119









120
121
122
123


124
125
126
127
128
129
130
131
132
133


134
135
136
137

138
139

140
141


142
143
144
145
146
147
148


149
150

151

152





+
-
+



-
+








-
+

-
+
+

-
+

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

-
-
-
+
+
+


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

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



+
+
+
-
-
+
+




-
+


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

+
-
-
+
+
+
+
+
+
+



-
-
+
+


-

+
-
+

-
-
+
+
+
+
+


-
-
+
+
-

-
+
id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210525121644
modified: 20240220190138

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 placed.
For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored.
An attacker that is able to change the owner can do anything.
Therefore only the owner of the computer on which Zettelstore runs can change this information.

The file for startup configuration must be created via a text editor in advance.

The syntax of the configuration file is the same as for any zettel metadata.
The following keys are supported:

; [!admin-port]''admin-port''
; [!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 administrators console.
  A value of ""0"" (the default) disables the administrator console.
  The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]].

  On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended).
  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''
; [!default-dir-place-type]''default-dir-place-type''
: Specifies the default value for the (sub-) type of [[directory places|00001004011400#type]].
  Zettel are typically stored in such places.
  Default: ""0""
; [!asset-dir|''asset-dir'']
: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server.
  The URL prefix for these files is ''/assets/''.
  You can use this if you want to transfer files that are too large for a note to users.
  Examples would be presentation files, PDF files, music files or video files.

  Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].]

  If you specify only the URL prefix, then the contents of the directory are listed to the user.
  To avoid this, create an empty file in the directory named ""index.html"".

  Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid.
; [!base-url|''base-url'']
: Sets the absolute base URL for the service.

  Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start.
  
  Default: ""http://127.0.0.1:23123/"".
; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one
: Specifies a [[box|00001004011200]] where zettel are stored.
  During startup __X__ is counted up, starting with one, until no key is found.
  This allows to configure more than one box.

  If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"".
  In this case, even a key ''box-uri-2'' will be ignored.
; [!debug-mode|''debug-mode'']
: Allows to debug the Zettelstore software (mostly used by the developers) if set to [[true|00001006030500]]
  Disables any timeout values of the internal web server and does not send some security-related data.
  Sets [[''log-level''|#log-level]] to ""debug"".

  Do not enable it for a production server.

  Default: ""false""
; [!default-dir-box-type|''default-dir-box-type'']
: Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]].
  Zettel are typically stored in such boxes.

  Default: ''notify''
; [!insecure-cookie]''insecure-cookie''
: Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP).
  Default: ""notify""
; [!insecure-cookie|''insecure-cookie'']
: Must be set to [[true|00001006030500]], if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP).
  Otherwise web browser are free to ignore the authentication cookie.

  Default: ''false''
; [!listen-addr]''listen-addr''
: Configures the network address, where is zettel web service is listening for requests.
  Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ''0.0.0.0'' if you want to listen on all network interfaces, and ''PORT'' is the TCP port.
  Default: ""false""
; [!insecure-html|''insecure-html'']
: Allows to use HTML, e.g. within supported markup languages, even if this might introduce security-related problems.
  However, HTML containing the ``<script>`` or the ``<iframe>`` tag is always ignored.
  But due to ""clever"" ways of combining HTML, CSS, JavaScript, there might be some negative security consequences.
  Please be aware of this!

  Allowed values: ""html"" (allow zettel with [[syntax ""html""|00001008000000#html]]), ""markdown"" (""html"", plus allow inline HTML for Markdown markup only), ""zettelmarkup"" (""markdown"", plus allow inline HTML for Zettelmarkup).
  Any other value is interpreted as ""secure"".

  Default: ""secure"".
; [!listen-addr|''listen-addr'']
: Configures the network address, where the Zettelstore service is listening for requests.
  Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port.

  Default value: ''"127.0.0.1:23123"''
; [!owner]''owner''
  Default value: ""127.0.0.1:23123""
; [!log-level|''log-level'']
: Specify the [[logging level|00001004059700]] for the whole application or for a given (internal) service, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]].
  Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]].

  Several specifications are separated by the semicolon character (""'';''"", U+003B).
  Each specification consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level.

  Default: ""info"".

  Examples: ""error"" will produce just error messages (e.g. no ""info"" messages); ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components.

  When you are familiar to operate the Zettelstore, you might set the level to ""error"" to receive less noisy messages from the Zettelstore.
; [!max-request-size|''max-request-size'']
: Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources.
  The minimum value is 1024.

  Default: 16777216 (16 MiB). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.
  The owner has full authorization for the Zettelstore.
  Only if owner is set to some value, user [[authentication|00001010000000]] is enabled.

  Ensure that key [[''secret''|#secret]] is set to a value of at least 16 bytes.
  Otherwise the Zettelstore will not start for security reasons.
; [!persistent-cookie]''persistent-cookie''
: A boolean value to make the access cookie persistent.
; [!persistent-cookie|''persistent-cookie'']
: A [[boolean value|00001006030500]] to make the access cookie persistent.
  This is helpful if you access the Zettelstore via a mobile device.
  On these devices, the operating system is free to stop the web browser and to remove temporary cookies.
  Therefore, an authenticated user will be logged off.

  If ''true'', a persistent cookie is used.
  If ""true"", a persistent cookie is used.
  Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds.

  Default: ''false''
  Default: ""false""
; [!place-uri-X]''place-uri-//X//'', where //X// is a number greater or equal to one
: Specifies a [[place|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 place.

  If no ''place-uri-1'' key is given, the overall effect will be the same as if only ''place-uri-1'' was specified with the value ''dir://.zettel''.
  In this case, even a key ''place-uri-2'' will be ignored.
; [!read-only-mode]''read-only-mode''
: Puts the Zettelstore web service into a read-only mode.
; [!read-only-mode|''read-only-mode'']
: Puts the Zettelstore service into a read-only mode, if set to a [[true value|00001006030500]].
  No changes are possible.

  Default: false.
; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html''
  Default: ""false"".
; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be modified by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  It is only needed to set this value, if [[authentication is enabled|00001010040100]] by setting key [[''owner''|#owner]] to some user identification.
; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its API.
  Default: 10.
  ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]].
  Default: ""10"".

  ''token-lifetime-html'' specifies the lifetime for the HTML views.
  Default: 60.
  It is automatically extended, when a new HTML view is rendered.
  Default: ""60"".
; [!url-prefix]''url-prefix''
; [!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'').
  Default: ''"/"''.
  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''
: Be more verbose inf logging data.
; [!verbose-mode|''verbose-mode'']
: Be more verbose when logging data, if set to a [[true value|00001006030500]].
  Default: false

Other keys will be ignored.
  Default: ""false""

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

2
3
4
5

6
7
8
9

10
11
12
13
14
15



16
17
18
19

20
21

22
23
24

25
26
27
28


29
30
31
32
33


34
35
36
37
38
39




40
41
42
43
44


45
46
47

-
+



-
+



-
+





-
-
-
+
+
+

-
+

-
+


-
+



-
-
+
+



-
-
+
+


+

-
-
-
-
+
+
+
+

-
-
+
+

id: 00001004011200
title: Zettelstore places
title: Zettelstore boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210525121452
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 in other places.
Under certain circumstances you may want to store your zettel elsewhere.

An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore.
They are stored within the software itself.
In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting.

To cope with these (and more) situations, you configure Zettelstore to use one or more places.
This is done via the ''place-uri-X'' keys of the [[startup configuration|00001004010000#place-uri-X]] (X is a number).
Places are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses.
To cope with these (and more) situations, you configure Zettelstore to use one or more __boxes__.
This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number).
Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses.

The following place URIs are supported:
The following box URIs are supported:

; ''dir:\//DIR''
; [!dir|''dir://DIR'']
: Specifies a directory where zettel files are stored.
  ''DIR'' is the file path.
  Although it is possible to use relative file paths, such as ''./zettel'' (&rarr; URI is ''dir:\//.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''.
  Although it is possible to use relative file paths, such as ''./zettel'' (&rarr; URI is ''dir://.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''.

  The directory must exist before starting the Zettelstore[^There is one exception: when Zettelstore is [[started without any parameter|00001004050000]], e.g. via double-clicking its icon, an directory called ''./zettel'' will be created.].

  It is possible to [[configure|00001004011400]] a directory place.
; ''file:FILE.zip'' oder ''file:/\//path/to/file.zip''
  It is possible to [[configure|00001004011400]] a directory box.
; [!file|''file:FILE.zip'' or ''file:///path/to/file.zip'']
: Specifies a ZIP file which contains files that store zettel.
  You can create such a ZIP file, if you zip a directory full of zettel files.

  This place is always read-only.
; ''mem:''
  This box is always read-only.
; [!mem|''mem:'']
: Stores all its zettel in volatile memory.
  If you stop the Zettelstore, all changes are lost.
  To limit usage of volatile memory, you should [[configure|00001004011600]] this type of box, although the default values might be valid for your use case.

All places that you configure via the ''store-uri-X'' keys form a chain of places.
If a zettel should be retrieved, a search starts in the place specified with the ''place-uri-2'' key, then ''place-uri-3'' and so on.
If a zettel is created or changed, it is always stored in the place specified with the ''place-uri-1'' key.
This allows to overwrite zettel from other places, e.g. the predefined zettel.
All boxes that you configure via the ''box-uri-X'' keys form a chain of boxes.
If a zettel should be retrieved, a search starts in the box specified with the ''box-uri-2'' key, then ''box-uri-3'' and so on.
If a zettel is created or changed, it is always stored in the box specified with the ''box-uri-1'' key.
This allows to overwrite zettel from other boxes, e.g. the predefined zettel.

If you use the ''mem:'' place, where zettel are stored in volatile memory, it makes only sense if you configure it as ''place-uri-1''.
Such a place will be empty when Zettelstore starts and only the first place will receive updates.
If you use the ''mem:'' box, where zettel are stored in volatile memory, it makes only sense if you configure it as ''box-uri-1''.
Such a box will be empty when Zettelstore starts and only the first box will receive updates.
You must make sure that your computer has enough RAM to store all zettel.

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

1

2
3
4
5

6
7


8
9
10
11
12
13

14



15
16
17
18
19
20

21
22

23
24
25
26
27
28
29

30
31
32
33
34





















35
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: 00001004011400
title: Configure file directory places
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210525121232
modified: 20220724200512

Under certain circumstances, it is preferable to further configure a file directory place.
This is done by appending query parameters after the base place URI ''dir:\//DIR''.
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:|
|type|(Sub-) Type of the directory service|(value of ''[[default-dir-place-type|00001004010000#default-dir-place-type]]'')
|type|(Sub-) Type of the directory service|(value of ""[[default-dir-box-type|00001004010000#default-dir-box-type]]"")
|rescan|Time (in seconds) after which the directory should be scanned fully|600
|worker|Number of worker that can access the directory in parallel|(depends on type)
|readonly|Allow only operations that do not change a zettel or create a new zettel|n/a
|worker|Number of worker that can access the directory in parallel|7
|readonly|Allow only operations that do not create or change zettel|n/a

=== Type
On some operating systems, Zettelstore tries to detect changes to zettel files outside of Zettelstore's control[^This includes Linux, Windows, and macOS.].
On other operating systems, this may be not possible, due to technical limitations.
Automatic detection of external changes is also not possible, if zettel files are placed on an external service, such as a file server accessed via SMD/CIFS or NFS.
Automatic detection of external changes is also not possible, if zettel files are put on an external service, such as a file server accessed via SMB/CIFS or NFS.

To cope with this uncertainty, Zettelstore provides various internal implementations of a directory place.
To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box.
The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual.
The following values are supported:

; simple
: Is not able to detect external changes.
  Works on all platforms.
  Is a little slower than other implementations (up to three times slower).
  Is a little slower than other implementations (up to three times).
; notify
: Automatically detect external changes.
  Tries to optimize performance, at a little cost of main memory (RAM).

=== Rescan
When the parameter ''type'' is set to ""notify"", Zettelstore automatically detects changes to zettel files that originates from other software.
It is done on a ""best-effort"" basis.
Under certain circumstances it is possible that Zettelstore does not detect a change done by another software.

To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file directory.
The time interval is configured by the ''rescan'' parameter, e.g.
```
place-uri-1: dir:///home/zettel?rescan=300
```
This makes Zettelstore to re-scan the directory ''/home/zettel/'' every 300 seconds, i.e. 5 minutes.

For a Zettelstore with many zettel, re-scanning the directory may take a while, especially if it is stored on a remote computer (e.g. via CIFS/SMB or NFS).
In this case, you should adjust the parameter value.

Please note that a directory re-scan invalidates all internal data of a Zettelstore.
It might trigger a re-build of the backlink database (and other internal databases).
Therefore a large value if preferred.

This value is ignored for other directory place type, such as ""simple"".

=== Worker
Internally, Zettelstore parallels concurrent requests for a zettel or its metadata.
The number of parallel activities is configured by the ''worker'' parameter.

A computer contains a limited number of internal processing units (CPU).
Its number ranges from 1 to (currently) 128, e.g. in bigger server environments.
Zettelstore typically runs on a system with 1 to 8 CPUs.
Access to zettel file is ultimately managed by the underlying operating system.
Depending on the hardware and on the type of the directory place, only a limited number of parallel accesses are desirable.
Depending on the hardware and on the type of the directory box, only a limited number of parallel accesses are desirable.

On smaller hardware[^In comparison to a normal desktop or laptop computer], such as the [[Raspberry Zero|https://www.raspberrypi.org/products/raspberry-pi-zero/]], a smaller value might be appropriate.
Every worker needs some amount of main memory (RAM) and some amount of processing power.
On bigger hardware, with some fast file services, a bigger value could result in higher performance, if needed.

For a directory place of type ""notify"", the default value is: 7.
The directory place type ""simple"" limits the value to a maximum of 1, i.e. no concurrency is possible with this type of directory place.

For various reasons, the value should be a prime number, with a maximum value of 1499.
For various reasons, the value should be a prime number.
The software might enforce this restriction by selecting the next prime number of a specified non-prime value.
The default value is 7, the minimum value is 1, the maximum value is 1499.

=== Readonly
Sometimes you may want to provide zettel from a file directory place, but you want to disallow any changes.
If you provide the query parameter ''readonly'' (with or without a corresponding value), the place will disallow any changes.
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.
```
place-uri-1: dir:///home/zettel?readonly
box-uri-1: dir:///home/zettel?readonly
```
If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory places will be in read-only mode too, even if not explicitly configured.
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.

Added docs/manual/00001004011600.zettel.


























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001004011600
title: Configure memory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20220307122554

Under most circumstances, it is preferable to further configure a memory box.
This is done by appending query parameters after the base box URI ''mem:''.

The following parameters are supported:

|= Parameter:|Description|Default value:|Maximum value:
|max-bytes|Maximum number of bytes the box will store|65535|1073741824 (1 GiB)
|max-zettel|Maximum number of zettel|127|65535

The default values are somehow arbitrarily, but applicable for many use cases.

While the number of zettel should be easily calculable by an user, the number of bytes might be a little more difficult.

Metadata consumes 6 bytes for the zettel identifier and for each metadata value one byte for the separator, plus the length of key and data.
Then size of the content is its size in bytes.
For text content, its the number of bytes for its UTF-8 encoding.

If one of the limits are exceeded, Zettelstore will give an error indication, based on the HTTP status code 507.

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

84
1
2
3
4
5
6
7
8
9
10


11
12
13
14
15

16
17
18
19
20



21
22
23
24







25












26
27
28



29
30
31
32
33


34
35





36
37
38
39
40
41
42
43
44
45








46
47
48
49
50
51
52
53
54
55
56
57
58
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: 20231126180829
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:
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''
; [!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-lang]''default-lang''
: Default language to be used when displaying content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''lang''. Default: ''en''.
; [!default-license|''default-license'']
: License value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''.

  This value 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]].
; [!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-role]''default-role''
: Role to be used, if a zettel specifies no ''role'' [[meta key|00001006020000]].
  Default: ''zettel''.
; [!default-syntax]''default-syntax''
: Syntax to be used, if a zettel specifies no ''syntax'' [[meta key|00001006020000]].
  Default: ''zmk'' (""Zettelmarkup"").
; [!default-title]''default-title''
: Title to be used, if a zettel specifies no ''title'' [[meta key|00001006020000]].
  Default: ''Untitled''.

  You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup.
; [!default-visibility]''default-visibility''
; [!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, all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if authentication is enabled; to all, otherwise).
  Default: ""login"".
; [!expert-mode|''expert-mode'']
: If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise).
  This affects most computed zettel.

  Default: False.
; [!footer-html]''footer-html''
  Default: ""False"".
; [!footer-zettel|''footer-zettel'']
: Contains some HTML code that will be included into the footer of each Zettelstore web page.
  It only affects the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected.
  Default: (the empty string).
; [!home-zettel]''home-zettel''
: Identifier of a zettel that is rendered as HTML and will be placed as the footer of every zettel in the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as symbolic expressions, etc. is not affected.
  If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer.

  May be [[overwritten|00001004020200]] in a user zettel.

  Default: (an invalid zettel identifier)
; [!home-zettel|''home-zettel'']
: Specifies the identifier of the zettel, that should be presented for the default view / home view.
  If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown.
; [!marker-external]''marker-external''
: Some HTML code that is displayed after a reference to external material.
  Default: ''&\#10138;'', to display a ""&#10138;"" sign.
; [!list-page-size]''list-page-size''
: If set to a value greater than zero, specifies the number of items shown in WebUI lists.
  Basically, this is the list of all zettel (possibly restricted) and the list of search results.
  Default: ''0''.
; [!site-name]''site-name''

  May be [[overwritten|00001004020200]] in a user zettel.

; [!lang|''lang'']
: Language to be used when displaying content.

  Default: ""en"".

  This value is used as a default value, if it is not set in an user's zettel or in a zettel.
  It is also used to specify the language for all non-zettel content, e.g. lists or search results.

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

  Default: ""1024"".
; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-subordinate-links|''show-subordinate-links''], [!show-successor-links|''show-successor-links'']
: When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel.
  This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''subordinates''|00001006020000#subordinates]], and  [[''successors''|00001006020000#successors]].

  These configuration keys may be used to show, not to show, or to close the list of referenced zettel.

  Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list).

  Default: """".

  May be [[overwritten|00001004020200]] in a user zettel, so that setting will only affect the given user.
  Alternatively, it may be overwritten in a zettel, so that that the setting will affect only the given zettel.

  This zettel is an example of a zettel that sets ''show-back-links'' to ""false"".
; [!site-name|''site-name'']
: Name of the Zettelstore instance.
  Will be used when displaying some lists.

  Default: ''Zettelstore''.
; [!yaml-header]''yaml-header''
: If true, metadata and content will be separated by ''-\--\\n'' instead of an empty line (''\\n\\n'').
  Default: ''false''.
  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
  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.
  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 ""meta"" and ""zmk"",
; [!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:
  Zettelstore will store the zettel as two files: one for the metadata
  (file extension ''.meta'') and one for the content (file extension based on
  one for the metadata (file without a filename extension) and another for the content (file extension based on the syntax value).
  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, duplicates are removed.
  All values are case-insensitive, duplicate values are removed.

  For example, you could use this key if you're working with Markdown syntax
  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.
  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
  If ''yaml-header'' evaluates to true, a zettel is always stored in one ''.zettel'' file.
  ''.zettel'' file.

Added docs/manual/00001004020200.zettel.























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

Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]].
A subset of those may be overwritten in zettel that is currently used.
This allows to specify user specific or zettel specific behavior.

The following metadata keys are supported to provide a more specific behavior:

|=Key|User:|Zettel:|Remarks
|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N|
|[[''home-zettel''|00001004020000#home-zettel]]|Y|N|
|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful
|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y|
|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y|
|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y|
|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y|

Changes to docs/manual/00001004050000.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
30
31
32
33
34
35
36
37
38





+
-
+

-
+
















-
+


-
+
+
+
+
+
+
+
+
+
+
id: 00001004050000
title: Command line parameters
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210511140731
modified: 20221128161932

Zettelstore is not just a web service that provides services of a zettelkasten.
Zettelstore is not just a service that provides services of a zettelkasten.
It allows to some tasks to be executed at the command line.
Typically, the task (""sub-command"") will be given at the command line as the first parameter.

If no parameter is given, the Zettelstore is called as
```
zettelstore
```
This is equivalent to call it this way:
```sh
mkdir -p ./zettel
zettelstore run -d ./zettel -c ./.zscfg
```
Typically this is done by starting Zettelstore via a graphical user interface by double-clicking to its file icon.
=== Sub-commands
* [[``zettelstore help``|00001004050200]] lists all available sub-commands.
* [[``zettelstore version``|00001004050400]] to display version information of Zettelstore.
* [[``zettelstore run``|00001004051000]] to start the web-based Zettelstore service.
* [[``zettelstore run``|00001004051000]] to start the Zettelstore service.
* [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI.
* [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services.
* [[``zettelstore password``|00001004051400]] to calculate data for user authentication.
* [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]].

Every sub-command allows the following command line options:
; [!h|''-h''] (or ''--help'')
: Does not execute the sub-command, but shows allowed command line options (except ''-h'' / ''--help'').
; [!l|''-l LOGSPEC'']
: Makes the given logging level specification effective for this command.
  Details, including syntax, can be found in the description for the [[''log-level''|00001004010000#log-level]] key of the startup configuration.

To measure potential bottlenecks within the software Zettelstore, there are some [[command line flags for profiling the application|00001004059900]].

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





-
+







-







id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
precursor: 00001004050000
modified: 20210712233414

Lists all implemented sub-commands.

Example:
```
# zettelstore help
Available commands:
- "config"
- "file"
- "help"
- "password"
- "run"
- "run-simple"
- "version"
```

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





-
+




-
+

-
-
+
+
-
-
+

-
+





-
+
+

-
+
-

+
+
+
id: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
precursor: 00001004050000
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, the name of the computer running the Zettelstore, and an indication about the operating system and the processor architecture of that computer.
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.

The build version information is a string like ''v1.0.2-34-gf567a3''.
The part ""v1.0.2"" is the release version.
The build version information is a string like ''1.0.2+351ae138b4''.
The part ""1.0.2"" is the release version.
The string ""34"" specifies the number of internal patches, after the release was published.
""gf567a3"" is a code uniquely identify the version to the developer.
""+351ae138b4"" is a code uniquely identifying the version to the developer.

Everything after the release version is optional, eg. ""v1.4.3"" is a valid build version information too.
Everything after the release version is optional, eg. ""1.4.3"" is a valid build version information too.

Example:

```
# zettelstore version
Zettelstore (v0.0.4/go1.15) running on mycomputer (linux/amd64)
Zettelstore 1.0.2+351ae138b4 (go1.16.5@linux/amd64)
Licensed under the latest version of the EUPL (European Union Public License)
```
In this example, Zettelstore is running in the released version ""v.0.0.4"" and was compiled using [[Go, version 1.15|https://golang.org/doc/go1.15]].
In this example, Zettelstore is running in the released version ""1.0.2"" and was compiled using [[Go, version 1.16.5|https://golang.org/doc/go1.16]].
It runs on a computer named ""mycomputer"".
The software was build for running under a Linux operating system with an ""amd64"" processor.

The build version is also stored in the public, [[predefined|00001005090000]] zettel titled ""[[Zettelstore Version|00000000000001]]"".
However, to access this zettel, you need a [[running zettelstore|00001004051000]].

Changes to docs/manual/00001004051000.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
31
32

33
34
35
36
37
38
39

40
41

42
43
44


45
46
47
48





-
+
-





-
+


-
+


-
+



-
-
+
+



-
+





-
+






-
+

-
+


-
-
+
+


id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210510153318
modified: 20220724162050
precursor: 00001004050000

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v]
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]
```

; [!a]''-a PORT''
; [!a|''-a PORT'']
: Specifies the TCP port through which you can reach the [[administrator console|00001004100000]].
  See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details.
; [!c]''-c CONFIGFILE''
; [!c|''-c CONFIGFILE'']
: Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read.
  It is ignored, when the given file is not available, nor readable.

  Default: ''./.zscfg''. (''.\\.zscfg'' on Windows)), where ''.'' denotes the ""current directory"".
; [!d]''-d DIR''
  Default: tries to read the following files in the ""current directory"": ''zettelstore.cfg'', ''zsconfig.txt'', ''zscfg.txt'', ''_zscfg'', and ''.zscfg''. 
; [!d|''-d DIR'']
: Specifies ''DIR'' as the directory that contains all zettel.

  Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"".
; [!debug]''-debug''
; [!debug|''-debug'']
: Allows better debugging of the internal web server by disabling any timeout values.
  You should specify this only as a developer.
  Especially do not enable it for a production server.

  [[https://blog.cloudflare.com/exposing-go-on-the-internet/#timeouts]] contains a good explanation for the usefulness of sensitive timeout values.
; [!p]''-p PORT''
; [!p|''-p PORT'']
: Specifies the integer value ''PORT'' as the TCP port, where the Zettelstore web server listens for requests.

  Default: 23123.

  Zettelstore listens only on ''127.0.0.1'', e.g. only requests from the current computer will be processed.
  If you want to listen on network card to process requests from other computer, please use [[''listen-addr''|00001004010000#listen-addr]] of the configuration file as described below.
; [!r]''-r''
; [!r|''-r'']
: Puts the Zettelstore in read-only mode.
  No changes are possible via the web interface / via the API.
  No changes are possible via the [[web user interface|00001014000000]] / via the [[API|00001012000000]].

  This allows to publish your content without any risks of unauthorized changes.
; [!v]''-v''
: Be more verbose in writing logs.
; [!v|''-v'']
: Be more verbose when writing logs.

Command line options take precedence over [[configuration file|00001004010000]] options.

Changes to docs/manual/00001004051100.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
28
29





-
+
+





+
+
+
+
-
+

-
+
+





-
+



id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
precursor: 00001004050000
created: 20210126175322
modified: 20221128161922

=== ``zettelstore run-simple``
This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon.
It is s simplified variant of the [[''run'' sub-command|00001004051000]].

First, this sub-command checks if it can read a [[Zettelstore startup configuration|00001004010000]] file by trying the [[default values|00001004051000#c]].
If this is the case, ''run-simple'' just continues as the [[''run'' sub-command|00001004051000]], but ignores any command line options (including ''-d DIR'').[^This allows a [[curious user|00001003000000]] to become an intermediate user.]


It allows only to specify a zettel directory.
If no startup configuration was found, the sub-command allows only to specify a zettel directory.
The directory will be created automatically, if it does not exist.
This is the only difference to the ''run'' sub-command, where the directory must exists.
This is a difference to the ''run'' sub-command, where the directory must exists.
In contrast to the ''run'' sub-command, other command line parameter are not allowed.

```
zettelstore run-simple [-d DIR]
```

; [!d]''-d DIR''
; [!d|''-d DIR'']
: Specifies ''DIR'' as the directory that contains all zettel.

  Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"".

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





-
+
+











-
-
+
+
-
-
+









id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
precursor: 00001004050000
created: 20210126175322
modified: 20230316182711

Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout.
This allows Zettelstore to render files manually.
```
zettelstore file [-t FORMAT] [file-1 [file-2]]
```

; ''-t FORMAT''
: Specifies the output format.
  Supported values are:
  [[''html''|00001012920510]] (default),
  [[''djson''|00001012920503]],
  [[''json''|00001012920501]],
  [[''md''|00001012920513]],
  [[''shtml''|00001012920525]],
  [[''native''|00001012920513]],
  [[''raw''|00001012920516]],
  [[''sz''|00001012920516]],
  [[''text''|00001012920519]],
  and [[''zmk''|00001012920522]].
; ''file-1''
: Specifies the file name, where at least metadata is read.
  If ''file-2'' is not given, the zettel content is also read from here.
; ''file-2''
: File name where the zettel content is stored.

If neither ''file-1'' nor ''file-2'' are given, metadata and zettel content are read from standard input / stdin.

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





-
+













-
+







id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
precursor: 00001004050000
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:
```
zettelstore password IDENT ZETTEL-ID
```

``IDENT`` is the identification for the user that should be authenticated.
``ZETTEL-ID`` is the [[identifier of the zettel|00001006050000]] that later acts as a user zettel.

See [[Creating an user zettel|00001010040200]] for background.
See [[Creating an user zettel|00001010040200]] for some background information.

An example:

```
# zettelstore password bob 20200911115600
Password:
   Again:

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001004059700
title: List of supported logging levels
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20211204182643
modified: 20240221134619

Zettelstore supports various levels of logging output.
This allows you to see the inner workings of Zettelstore, or to avoid it.

Each level has an associated name and number.
A lower number signals more logging output.

|= Name | Number :| Description
| Trace | 1 | Show most of the inner workings
| Debug | 2 | Show many internal values that might be interesting for a [[Zettelstore developer|00000000000005]].
| Info  | 3 | Display information about an event. In most cases, there is no required action expected from you.
| Error | 4 | Notify about an error, which was handled automatically. Something is broken. User intervention may be required, some important functionality may be disabled. Monitor the application.
| Mandatory | 5 | Important message will be shown, e.g. the Zettelstore version at startup time.
| Disabled | 6 | No messages will be shown

If you set the logging level to a certain value, only messages with the same or higher numerical value will be shown.
E.g. if you set the logging level to ""error"", no ""trace"", ""debug"", and ""info"" messages are shown, but ""error"" and ""mandatory"" messages.

Added docs/manual/00001004059900.zettel.

























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001004059900
title: Command line flags for profiling the application
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20211122174951

If you want to measure potential bottlenecks within the software Zettelstore,
there are two [[command line|00001004050000]] flags for enabling the measurement (also called __profiling__):

; ''-cpuprofile FILE''
: Enables CPU profiling.
  ''FILE'' must be the name of the file where the data is stored.
; ''-memprofile FILE''
: Enables memory profiling.
  ''FILE'' must be the name of the file where the data is stored.

Normally, profiling will stop when you stop the software Zettelstore.
The given ''FILE'' can be used to analyze the data via the tool ``go tool pprof FILE``.

Please notice that ''-cpuprofile'' takes precedence over ''-memprofile''.
You cannot measure both.

You also can use the [[administrator console|00001004100000]] to begin and end profiling manually for a already running Zettelstore.

Changes to docs/manual/00001004100000.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
18
19
20

21
22
23
24
25





-
+







-
+






-
+




id: 00001004100000
title: Zettelstore Administrator Console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210510155859
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]].

After you enable the administrator console, you can use tools such as [[PuTTY|https://www.chiark.greenend.org.uk/~sgtatham/putty/]] or other telnet software to connect to the administrator console.
In fact, the administrator console is //not// a full telnet service.
In fact, the administrator console is __not__ a full telnet service.
It is merely a simple line-oriented service where each input line is interpreted separately.
Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc.

After connecting to the administrator console, there is no further authentication.
It is not needed because you must be logged in on the same computer where Zettelstore is running.
You cannot connect to the administrator console if you are on a different computer.
Of course, on multi-user systems with untrusted users, you should not enable the administrator console.
Of course, on multi-user systems with encrusted users, you should not enable the administrator console.

* Enable via [[command line|00001004051000#a]]
* Enable via [[configuration file|00001004010000#admin-port]]
* [[List of supported commands|00001004101000]]

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

6
7

8
9

10
11
12

13
14

15
16
17
18

19
20

21
22
23
24



25
26
27
28
29
30

31
32

33
34
35
36
37
38
39
40



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

55
56
57
58
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: 00001004101000
title: List of supported commands of the administrator console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210525161623
modified: 20220823194553

; ''bye''
; [!bye|''bye'']
: Closes the connection to the administrator console.
; ''config SERVICE''
; [!config|''config SERVICE'']
: Displays all valid configuration keys for the given service.

  If a key ends with the hyphen-minus character (""''-''"", ''U+002D''), the key denotes a list value.
  If a key ends with the hyphen-minus character (""''-''"", U+002D), the key denotes a list value.
  Keys of list elements are specified by appending a number greater than zero to the key.
; ''crlf''
; [!crlf|''crlf'']
: Toggles CRLF mode for console output.
  Changes end of line sequences between Windows mode (==\\r\\n==) and non-Windows mode (==\\n==, initial value).
  Often used on Windows telnet clients that otherwise scramble the output of commands.
; ''dump-index''
; [!dump-index|''dump-index'']
: Displays the content of the internal search index.
; ''dump-recover RECOVER''
; [!dump-recover|''dump-recover RECOVER'']
: Displays data about the last given recovered internal activity.

  The value for ''RECOVER'' can be obtained via the command ``stat core``, which lists all overview data about all recoveries.
; ''echo''
: Toggles the echo mode, where each command is printed before execution
; ''env''
; [!echo|''echo'']
: Toggles the echo mode, where each command is printed before execution.
; [!end-profile|''end-profile'']
: Stops profiling the application.
; [!env|''env'']
: Display environment values.
; ''help''
; [!help|''help'']
: Displays a list of all available commands.
; ''get-config''
; [!get-config|''get-config'']
: Displays current configuration data.

  ``get-config`` shows all current configuration data.

  ``get-config SERVICE`` shows only the current configuration data of the given service.

  ``get-config SERVICE KEY`` shows the current configuration data for the given service and key.
; ''header''
: Toggles the header mode, where each table is show with a header nor not. 
; ''metrics''
; [!header|''header'']
: Toggles the header mode, where each table is show with a header nor not.
; [!log-level|''log-level'']
: Displays or sets the [[logging level|00001004059700]] for the kernel or a service.

  ``log-level`` shows all known log level.

  ``log-level NAME`` shows log level for the given service or for the kernel.

  ``log-level NAME VALUE`` sets the log level for the given service or for the kernel.
  ''VALUE'' is either the name of the log level or its numerical value.
; [!metrics|''metrics'']
: Displays some values that reflect the inner workings of Zettelstore.
  See [[here|https://golang.org/pkg/runtime/metrics/]] for a technical description of these values.
; ''next-config''
; [!next-config|''next-config'']
: Displays next configuration data.
  It will be the current configuration, if the corresponding services is restarted.

  ``next-config`` shows all next configuration data.

  ``next-config SERVICE`` shows only the next configuration data of the given service.

  ``next-config SERVICE KEY`` shows the next configuration data for the given service and key.
; [!profile|''profile [PROFILE] [FILE]'']
: Starts to profile the software with the profile PROFILE and writes profiling data to file FILE.
  If PROFILE is not given, a value ''CPU'' is assumed, which specifies to profile CPU usage.
  If FILE is not given, a value ''PROFILE.prof'' will be used.

  Other values for ''PROFILE'' are: ''goroutine'', ''heap'', ''allocs'', ''threadcreate'', ''block'', and ''mutex''.
  In the future, more values may be appropriate.
  See the [[Go documentation|https://pkg.go.dev/runtime/pprof#Profile]] for details.

  This feature is dependent on the internal implementation language of Zettelstore, Go.
  It may be removed without any further notice at any time.
  In most cases, it is a tool for software developers to optimize Zettelstore's internal workings.
; [!refresh|''refresh'']
: Refresh all internal data about zettel.
; ''restart SERVICE''
; [!restart|''restart SERVICE'']
: Restart the given service and all other that depend on this.
; ''services''
; [!services|''services'']
: Displays s list of all available services and their current status.
; ''set-config SERVICE KEY VALUE''
; [!set-config|''set-config SERVICE KEY VALUE'']
: Sets a single configuration value for the next configuration of a given service.
  It will become effective if the service is restarted.

  If the key specifies a list value, all other list values with a number greater than the given key are deleted.
  You can use the special number ""0"" to delete all values.
  E.g. ``set-config place place-uri-0 any_text`` will remove all values of the list //place-uri-//.
; ''shutdown''
  E.g. ``set-config box box-uri-0 any_text`` will remove all values of the list __box-uri-__.
; [!shutdown|''shutdown'']
: Terminate the Zettelstore itself (and closes the connection to the administrator console).
; ''start SERVICE''
: Start the given bservice and all dependent services.
; ''stat SERVICE''
; [!start|''start SERVICE'']
: Start the given service and all dependent services.
; [!stat|''stat SERVICE'']
: Display some statistical values for the given service.
; ''stop SERVICE''
; [!stop|''stop SERVICE'']
: Stop the given service and all other that depend on this.

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


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
79
80
81
82
83
84
85
86
87
88
89
90
91





+







-
+



-
+

-
+



-
+





-
+




-
+




-
-
-
+
+
+
+
+




-
+




-
+





-
-
+
+





-
-
+
+











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

Zettelstore is a software that manages your zettel.
Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories.
Typically, file names and file content must comply to specific rules so that Zettelstore can manage them.
If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions.

Zettelstore provides additional services to the user.
Via a builtin web interface you can work with zettel in various ways.
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) that allows other software to communicate with the Zettelstore.
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 web 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 build, 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 as files in a specific directory.
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 interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss'').
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 identifiers either with the __rename__ function of Zettelstore or by manually renaming the underlying zettel files.

It is allowed that the file name contains other characters after the 14 digits.
These are ignored by Zettelstore.

The file name must have an file extension.
Two file extensions are used by Zettelstore: ''.meta'' and ''.zettel''.
Other file extensions are used to determine the ""syntax"" of a zettel.
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.
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.

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 placed in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator"").
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.
The [[configuration zettel|00001004020000]] is one example.
To render the builtin web interface, some templates are used, as well as a layout specification in CSS.
The icon that visualizes an external link is a predefined SVG image.
To render the builtin [[web user interface|00001014000000]], some templates are used, as well as a [[layout specification in CSS|00000000020001]].
The icon that visualizes a broken image is a [[predefined GIF image|00000000040001]].
All of these are visible to the Zettelstore as zettel.

One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences.

Where are these zettel stored?
They are stored within the Zettelstore software itself, because one design goal was to have just one file to use Zettelstore.
But data stored within an executable programm cannot be changed later[^Well, it can, but it is a very bad idea to allow this. Mostly for security reasons.].
They are stored within the Zettelstore software itself, because one [[design goal|00001002000000]] was to have just one executable file to use Zettelstore.
But data stored within an executable program cannot be changed later[^Well, it can, but it is a very bad idea to allow this. Mostly for security reasons.].

To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together.
If you change a zettel, it will be always stored as a file.
If a zettel is requested, Zettelstore will first try to read that zettel from a file.
If such a file was not found, the internal zettel store is searched secondly.

Therefore, the file store ""shadows"" the internal zettel store.
If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier.
Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software.

* [[List of predefined zettel|00001005090000]]

=== Boxes: alternative ways to store zettel
As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself.
Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.]

A file directory which stores zettel is called a ""directory box"".
But zettel may be also stored in a ZIP file, which is called ""file box"".
For testing purposes, zettel may be stored in volatile memory (called __RAM__).
This way is called ""memory box"".

Other types of boxes could be added to Zettelstore.
What about a ""remote Zettelstore box""?

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

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

20
21
22
23
24
25
26

27
28

29
30
31
32







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





+
-
+










+
+
-
+

+




-
+

-
+



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



+


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

The following table lists all predefined zettel with their purpose.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore
| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore
| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content
| [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages
| [[00000000000008]] | Zettelstore Memory | Some statistics about main memory usage
| [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places and the the index process
| [[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 Meta HTML Template | Used when displaying a list of zettel
| [[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 Templöate | Layout for the information view of a specific 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
| [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles
| [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists
| [[00000000020001]] | Zettelstore Base CSS | CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000040001]] | Generic Emoji | Image that is shown if original image reference is invalid
| [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]""
| [[00000000090002]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]""
| [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message
| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates
| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid
| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]""
| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]""
| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]""
| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]""
| [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]
| [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]]
| [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]]
| [[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
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49


+


-
+
+

-
+















-
+



-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001006000000
title: Layout of a Zettel
role: manual
tags: #design #manual #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20230403123541

A zettel consists of two part: the metadata and the zettel content.
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.

Metadata has to conform to a [[special syntax|00001006010000]].
It is effectively a collection of key/value pairs.
Some keys have a [[special meaning|00001006020000]] and most of the predefined keys need values of a specific [[type|00001006030000]].

Each zettel is given a unique [[identifier|00001006050000]].
To some degree, the zettel identifier is part of the metadata..

The zettel content is your valuable content.
Zettelstore contains some predefined parsers that interpret the zettel content to the syntax of the zettel.
This includes markup languages, like [[Zettelmarkup|00001007000000]] and [[CommonMark|https://commonmark.org/]].
This includes markup languages, like [[Zettelmarkup|00001007000000]] and [[CommonMark|00001008010500]].
Other text formats are also supported, like CSS and HTML templates.
Plain text content is always Unicode, encoded as UTF-8.
Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.].
There is support for a graphical format with a text represenation: SVG.
And the is support for some binary image formats, like GIF, PNG, and JPEG.
There is support for a graphical format with a text representation: SVG.
And there is support for some binary image formats, like GIF, PNG, and JPEG.

=== Plain, parsed, and evaluated zettel
Zettelstore may present your zettel in various forms, typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''.
One way is to present the zettel as it was read by Zettelstore.
This is called ""[[plain zettel|00001012053300]]"".

The second way is to present the zettel as it was recognized by Zettelstore.
This is called ""[[parsed zettel|00001012053600]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'', but with the additional query parameter ''parseonly''.
Such a zettel was read and analyzed.
It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.]

However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'' and specifying an encoding.
The biggest difference to a parsed zettel is the inclusion of [[block transclusions|00001007031100]] or [[inline transclusions|00001007040324]] for an evaluated zettel.
It can also be presented in various encoding, including the ""zmk"" encoding.
Evaluations also applies to metadata of a zettel, if appropriate.

Please note, that searching for content is based on parsed zettel.
Transcluded content will only be found in transcluded zettel, but not in the zettel that transcluded the content.
However, you will easily pick up that zettel by follow the [[backward|00001006020000#backward]] metadata key of the transcluded zettel.

Changes to docs/manual/00001006010000.zettel.

1
2

3
4
5


6
7
8
9
10

11

12
13
14
15
16
17
18
19


20
21
22
23
24
25
26
1
2
3
4
5

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: 00001006010000
title: Syntax of Metadata
role: manual
tags: #manual #syntax #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20240219193158

The metadata of a zettel is a collection of key-value pairs.
The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]).

The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"") is also allowed.
The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"", U+002D) is also allowed.
It begins at the first position of a new line.
Uppercase letters of a key are translated to their lowercase equivalence.

A key is separated from its value either by
* a colon character (""'':''""),
* a non-empty sequence of space characters,
* a sequence of space characters, followed by a colon, followed by a sequence of space characters.

A Value is a sequence of printable characters.
If the value should be continued in the following line, that following line (//continuation line//) must begin with a non-empty sequence of space characters.
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.

Parsing metadata ends, if an empty line is found or if a line with at least three hyphen-minus characters is found.

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5


6
7
8
9
10
11



12

13
14
15


16
17
18





19
20











21

22
23
24
25
26
27
28










29


30

31
32

33
34
35

36
37
38



39
40

41
42
43

44
45
46
47
48
49
50
51

52
53




54

55
56


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

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





+
+






+
+
+
-
+

-
-
+
+

-
-
+
+
+
+
+


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




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

+
+
-
+

-
+


-
+


-
+
+
+

-
+


-
+





-
-
-
+


+
+
+
+
-
+

-
+
+



-
+



+
+
+
-
+


-
+



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


-
-
+
+

-
-
+
+

-
-
+
-
-
+


-
-
-
+
+
+
+
+
+
+



-
+




-
+




id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230704161159

Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore.
See the [[computed list of supported metadata keys|00000000000090]] for details.

Most keys conform to a [[type|00001006030000]].

; [!author|''author'']
: A string value describing the author of a zettel.
  If given, it will be shown in the [[web user interface|00001014000000]] for the zettel.
; [!back]''back''
; [!back|''back'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel.
  Basically, it is the value of [[''backward''|#bachward]], but without any zettel identifier that is contained in [[''forward''|#forward]].
; [!backward]''backward''
  Basically, it is the value of [[''backward''|#backward]], but without any zettel identifier that is contained in [[''forward''|#forward]].
; [!backward|''backward'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata.
  References within inversable values are not included here, e.g. [[''precursor''|#precursor]].
; [!copyright]''copyright''
  References within invertible values are not included here, e.g. [[''precursor''|#precursor]].
; [!box-number|''box-number'']
: Is a computed value and contains the number of the box where the zettel was found.
  For all but the [[predefined zettel|00001005090000]], this number is equal to the number __X__ specified in startup configuration key [[''box-uri-__X__''|00001004010000#box-uri-x]].
; [!copyright|''copyright'']
: Defines a copyright string that will be encoded.
  If not given, the value ''default-copyright'' from the  [[configuration zettel|00001004020000#default-copyright]] will be used.
; [!created|''created'']
: Date and time when a zettel was created through Zettelstore.
  If you create a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value.

  This is a computed value.
  There is no need to set it via Zettelstore.
  
  If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used.

  Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second.
  When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible.
; [!credential]''credential''
; [!credential|''credential'']
: Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]].
  It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key.

  It is only used for zettel with a ''role'' value of ""user"".
; [!dead]''dead''
: Property that contains all references that does //not// identify a zettel.
; [!folge]''folge''
; [!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''
; [!forward|''forward'']
: Property that contains all references that identify another zettel within the content of the zettel.
; [!id]''id''
; [!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''
; [!lang|''lang'']
: Language for the zettel.
  Mostly used for HTML rendering of the zettel.
  If not given, the value ''default-lang'' from the  [[configuration zettel|00001004020000#default-lang]] will be used.

  If not given, the value ''lang'' from the zettel of the [[current user|00001010040200]] will be used.
  If that value is also not available, it is read from the [[configuration zettel|00001004020000#lang]] will be used.
  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!license]''license''
; [!license|''license'']
: Defines a license string that will be rendered.
  If not given, the value ''default-license'' from the [[configuration zettel|00001004020000#default-license]] will be used.
; [!modified]''modified''
; [!modified|''modified'']
: Date and time when a zettel was modified through Zettelstore.
  If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value.

  This is a computed value.
  There is no need to set it via Zettelstore.
; [!no-index]''no-index''
: If set to true, the zettel will not be indexed and therefore not be found in full-text searches.
; [!precursor]''precursor''
; [!precursor|''precursor'']
: References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel.
  Basically the inverse of key [[''folge''|#folge]].
; [!predecessor|''predecessor'']
: References the zettel that contains a previous version of the content.
  In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons.
  Basically the inverse of key [[''successors''|#successors]].
; [!published]''published''
; [!published|''published'']
: This property contains the timestamp of the mast modification / creation of the zettel.
  If [[''modified''|#modified]]is set, it contains the same value.
  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|00001012051800#sort]] zettel based on their publication date.
  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''
; [!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''
; [!role|''role'']
: Defines the role of the zettel.
  Can be used for selecting zettel.
  See [[supported zettel roles|00001006020100]].
  If not given, the value ''default-role'' from the [[configuration zettel|00001004020000#default-role]] will be used.
; [!syntax]''syntax''
  If not given, it is ignored.
; [!subordinates|''subordinates'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value.
; [!successors|''successors'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value.
  Therefore, it references all zettel that contain a new version of the content and/or metadata.
  In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons.
  In most cases, zettel referencing the current zettel should be updated to reference a successor zettel.
  The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel.
; [!summary|''summary'']
: Summarizes the content of the zettel.
  You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup.
; [!superior|''superior'']
: Specifies a zettel that is conceptually a superior zettel.
  This might be a more abstract zettel, or a zettel that should be higher in a hierarchy.
; [!syntax|''syntax'']
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If not given, the value ''default-syntax'' from the [[configuration zettel|00001004020000#default-syntax]] will be used.
; [!tags]''tags''
  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''
  Each Tag must begin with the number sign character (""''#''"", U+0023).
; [!title|''title'']
: Specifies the title of the zettel.
  If not given, the value ''default-title'' from the [[configuration zettel|00001004020000#default-title]] will be used.

  If not given, the value of [[''id''|#id]] will be used.
  You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup.
; [!url]''url''
; [!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 on the web user interface if you use the default template.
; [!user-id]''user-id''
: Provides some unique user identification for a user zettel.
  The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template.
; [!useless-files|''useless-files'']
: Contains the file names that are rejected to serve the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].
  If a zettel is renamed or deleted, these files will be deleted.
; [!user-id|''user-id'']
: Provides some unique user identification for an [[user zettel|00001010040200]].
  It is used as a user name for authentication.

  It is only used for zettel with a ''role'' value of ""user"".
; [!user-role]''user-role''
; [!user-role|''user-role'']
: Defines the basic privileges of an authenticated user, e.g. reading / changing zettel.
  Is only valid in a user zettel.

  See [[User roles|00001010070300]] for more details.
; [!visibility]''visibility''
; [!visibility|''visibility'']
: When you work with authentication, you can give every zettel a value to decide, who can see the zettel.
  Its default value can be set with [[''default-visibility''|00001004020000#default-visibility]] of the configuration zettel.

  See [[visibility rules for zettel|00001010070200]] for more details.

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

43





+
+


-
-
-
-
-
-
-
+
+
-



-
-
+
+

-
+

-
+
+
+
+
+
+
+
+
+
+



-
+



-
+


-
-
+
+

-
+
id: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231129173620

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
The following values are used internally by Zettelstore and must exist:

; [!user]''user''
: If you want to use [[authentication|00001010000000]], all zettel that identify users of the zettel store must have this role.

Beside of this, you are free to define your own roles.

You are free to define your own roles.
It is allowed to set an empty value or to omit the role.
The role ''zettel'' is predefined as the default role, but you can [[change this|00001004020000#default-role]].

Some roles are defined for technical reasons:

; [!configuration]''configuration''
: A zettel that contains some configuration data for the Zettelstore.
; [!configuration|''configuration'']
: A zettel that contains some configuration data / information for the Zettelstore.
  Most prominent is [[00000000000100]], as described in [[00001004020000]].
; [!manual]''manual''
; [!manual|''manual'']
: All zettel that document the inner workings of the Zettelstore software.
  This role is used in this specific Zettelstore.
  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''
; [!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''
; [!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 raw text of a scientific paper, ''project'' to define a project, ...
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
13
14
15
16
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: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20211124132040

A zettel can be marked as read-only, if it contains a metadata value for key
[[''read-only''|00001006020000#read-only]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel,
depending on their [[user role|00001010070300]].
Otherwise, the read-only mark is just a binary value.

=== No authentication
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]]
is interpreted as ""false"", anybody can modify the zettel.

If the metadata value is something else (the value ""true"" is recommended),
the user cannot modify the zettel through the web interface.
However, if the zettel is stored as a file in a [[directory place|00001004011400]],
the user cannot modify the zettel through the [[web user interface|00001014000000]].
However, if the zettel is stored as a file in a [[directory box|00001004011400]],
the zettel could be modified using an external editor.

=== Authentication enabled
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]]
is interpreted as ""false"", anybody can modify the zettel.

If the metadata value is the same as an explicit [[user role|00001010070300]],
users with that role (or a role with lower rights) are not allowed to modify the zettel.

; ""reader""
: Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel.
  Users with role ""writer"" or the owner itself still can modify the zettel.
; ""writer""
: Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel.
  Only the owner of the Zettelstore can modify the zettel.

If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended),
no user is allowed modify the zettel through the web interface.
However, if the zettel is accessible as a file in a [[directory place|00001004011400]],
no user is allowed modify the zettel through the [[web user interface|00001014000000]].
However, if the zettel is accessible as a file in a [[directory box|00001004011400]],
the zettel could be modified using an external editor.
Typically the owner of a Zettelstore have such an access.

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

9
10
11


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


28
29
30

31
32
33
34
35
36
37
38
39
40

41





+
+

-
+

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



-
-
+
+

-










-

id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240219161909

Most [[supported metadata keys|00001006020000]] conform to a type.
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.
Every metadata key should conform to a type.
User-defined metadata keys are of type EString.

|=Suffix|Type
| ''-date'' | [[Timestamp|00001006034500]]
| ''-number'' | [[Number|00001006033000]]
| ''-role'' | [[Word|00001006035500]]
| ''-time'' | [[Timestamp|00001006034500]]
| ''-title'' | [[Zettelmarkup|00001006036500]]
| ''-url'' | [[URL|00001006035000]]
| ''-zettel''  | [[Identifier|00001006032000]]
| ''-zid''  | [[Identifier|00001006032000]]
| ''-zids''  | [[IdentifierSet|00001006032500]]
| any other suffix | [[EString|00001006031500]]

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

Every key type has an associated validation rule to check values of the given type.
There is also a rule how values are matched, e.g. against a search term when selecting some zettel.
And there is a rule, how values compare for sorting.
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.

* [[Boolean|00001006030500]]
* [[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
13
14
15
16
17
18
19
20


21
1

2
3

4
5
6
7

8
9



10
11
12








13
14


-
+

-
+

+

-
+

-
-
-
+
+

-
-
-
-
-
-
-
-
+
+
-
id: 00001006030500
title: Boolean Key Type
title: Boolean Value
role: manual
tags: #manual #meta #reference #zettel #zettelstore
tags: #manual #reference #zettel #zettelstore
syntax: zmk
modified: 20220304114040

Values of this type denote a truth value.
On some places, metadata values are interpreted as a truth value.

=== Allowed values
Every character sequence that begins with a ""''0''"", ""''F''"", ""''N''"", ""''f''"", or a ""''n''"" is interpreted as the ""false"" boolean value.
All other metadata value is interpreted as the ""true"" boolean 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.

=== Match operator
The match operator is the equals operator, i.e.
* ``(true == true) == true``
* ``(false == false) == true``
* ``(true == false) == false``
* ``(false == true) == false``

=== Sorting
However, there is no full support for a boolean type.
Such values are interpreted as a [[string value|00001006033500]], e.g. with respect to comparing and sorting.
The ""false"" value is less than the ""true"" value: ``false < true``

Changes to docs/manual/00001006031000.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
17
18
19
20





+
+







-
-
+
+
+



id: 00001006031000
title: Credential Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175515

Values of this type denote a credential value, e.g. an encrypted password.

=== Allowed values
All printable characters are allowed.
Since a credential contains some kind of secret, the sequence of characters might have some hidden syntax to be interpreted by other parts of Zettelstore.

=== Match operator
A credential never matches to any other value.
=== Query comparison
A credential never compares to any other value.
A comparison will never match in any way.

=== Sorting
If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead.

Changes to docs/manual/00001006031500.zettel.

1
2
3
4
5


6
7
8
9
10
11
12
13
14

15
16

17
18
19
20
21
22
23
24
25
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





+
+








-
+
-
-
+
-
-








id: 00001006031500
title: EString Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175525

Values of this type are just a sequence of character, possibly an empty sequence.

An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].]

=== Allowed values
All printable characters are allowed.

=== Match operator
=== Query comparison
A value matches an EString value, if the first value is part of the EString value.
This check is done case-insensitive.
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

For example, ""hell"" matches ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001006032000.zettel.

1
2
3
4
5


6
7
8
9
10
11


12
13









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


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





+
+






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







id: 00001006032000
title: Identifier Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230612183459

Values of this type denote a [[zettel identifier|00001006050000]].

=== Allowed values
Must be a sequence of 14 digits (""0""--""9"").

=== Query comparison
[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters.
=== Match operator
A value matches an identifier value, if the first value is the prefix of the identifier value.

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

11
12
13
14
15


16
17
18

19
20
21
22





+
+



-
+




-
-
+
+

-
+



id: 00001006032500
title: IdentifierSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175551

Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]].

A set is different to a list, as no duplicates are allowed.
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.

=== Match operator
A value matches an identifier set value, if the first value is a prefix of one of the identifier value.
=== Query comparison
A value matches an identifier set value, if the value matches any of the identifier set values.

For example, ""000010"" matches ""[[00001006032000]] [[00001006032500]]"".
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: 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.

=== Match operator
The match operator is the equals operator, i.e. two values must be numeric equal to match.
=== 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.
This includes that ""+12"" is equal to ""12"", therefore both values match.
In this case, the number ""+12"" will be treated as different to the number ""12"".

=== Sorting
Sorting is done by comparing the numeric values.

Changes to docs/manual/00001006033500.zettel.

1
2
3
4
5


6
7
8
9
10
11
12
13

14
15

16
17
18
19
20
21
22
23
24
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





+
+







-
+
-
-
+
-
-








id: 00001006033500
title: String Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175633

Values of this type are just a sequence of character, but not an empty sequence.

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Match operator
=== Query comparison
A value matches a String value, if the first value is part of the String value.
This check is done case-insensitive.
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

For example, ""hell"" matches ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001006034000.zettel.

1
2
3
4
5


6
7
8
9

10
11
12

13
14
15
16

17
18
19
20
21

22
23

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





+
+



-
+


-
+


-
-
+

-
-
-
-
+
-
-
+



id: 00001006034000
title: TagSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175642

Values of this type denote a (sorted) set of tags.

A set is different to a list, as no duplicates are allowed.
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.
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.

=== Match operator
It depends of the first character of a search string how it is matched against a tag set value:
All characters are mapped to their lower case values.

* If the first character of the search string is a number sign character,
  it must exactly match one of the values of a tag.
* In other cases, the search string must be the prefix of at least one tag.

=== Query comparison
Conpectually, all number sign characters are removed at the beginning of the search string
and of all tags.
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
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
33
34
35
36

37







+
-
+




-
+








-
-
+
+
+
+
+
+
+

+
+
+
-
+
+
+


-
+
-
-
id: 00001006034500
title: Timestamp Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20210511131903
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.

=== Match operator
A value matches a timestamp value, if the first value is the prefix of the timestamp value.
If the sequence is less than 14 digits, they are expanded with the following rule:

* YYYY is expanded to YYYY0101000000
* YYYYMM is expanded to YYYYMM01000000
* YYYYMMDD is expanded to YYYYMMDD000000
* YYYYMMDDhh is expanded to YYYYMMDDhh0000
* YYYYMMDDhhmm is expanded to YYYYMMDDhhmm00

=== Query comparison
[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters.
Then, they are treated as timestamp data, as describe above, if they contain 4, 6, 8, 10, or 12 digits.
For example, ""202102"" matches ""20210212143200"".

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

Values of this type denote an URL.

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

=== Match operator
=== Query comparison
A value matches a URL value, if the first value is part of the URL value.
This check is done case-insensitive.
All comparisons are done case-insensitive.

For example, ""hell"" matches ""http://example.com/Hello"".
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
1
2
3
4
5
6
7
8
9
10
11
12
13


14
15
16
17
18
19
20





+
+






-
-
+
+
+
+



id: 00001006035500
title: Word Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175735

Values of this type denote a single word.

=== Allowed values
Must be a non-empty sequence of characters, but without the space character.

=== Match operator
A value matches a word value, if both value are character-wise equal.
All characters are mapped to their lower case values.

=== Query comparison
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Deleted docs/manual/00001006036000.zettel.

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


















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001006036000
title: WordSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a (sorted) set of [[words|00001006035500]].

A set is different to a list, as no duplicates are allowed.

=== Allowed values
Must be a sequence of at least one word, separated by space characters.

=== Match operator
A value matches an wordset value, if the first value is equal to one of the word values in the word set.

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Changes to docs/manual/00001006036500.zettel.

1
2
3
4
5


6
7
8
9
10
11
12
13
14
15



16
17

18
19
20
21
22
23
24
25
1
2
3
4
5
6
7
8
9
10
11
12
13
14



15
16
17
18

19
20
21
22
23
24
25
26
27





+
+







-
-
-
+
+
+

-
+








id: 00001006036500
title: Zettelmarkup Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175441

Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]].

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Match operator
A value matches a String value, if the first value is part of the String value.
This check is done case-insensitive.
=== 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, ""hell"" matches ""Hello"".
For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001006050000.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: 00001006050000
title: Zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
role: manual
modified: 20210721123222

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the
next two represent the month, following by day, hour, minute, and second.

This allows to order zettel chronologically in a canonical way.

In most cases the zettel identifier is the timestamp when the zettel was created.

However, the Zettelstore software just checks for exactly 14 digits.
Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with
a month part of ""35"" or with ""99"" as the last two digits.
In fact, all identifiers of zettel initially provided by an empty Zettelstore
begin with ""000000"", except the home zettel ''00010000000000''.

Some zettel identifier are [[reserved|00001006055000]] and should not be used otherwise.
All identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''.
Zettel identifier of this manual have be chosen to begin with ""000010"".

A zettel can have any identifier that contains 14 digits and that is not in use
by another zettel managed by the same Zettelstore.

Added docs/manual/00001006055000.zettel.













































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

[[Zettel identifier|00001006050000]] are typically created by examine the current date and time.
By renaming a zettel, you are able to provide any sequence of 14 digits.
If no other zettel has the same identifier, you are allowed to rename a zettel.

To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000'').

All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.].
Zettel identifier of this manual have be chosen to begin with ''000010''.

However, some external applications may need a range of specific zettel identifier to work properly.
Identifier that begin with ''00009'' can be used for such purpose.
To request a reservation, please send an email to the maintainer of Zettelstore.
The request must include the following data:
; Title
: Title of you application
; Description
: A brief description what the application is used for and why you need to reserve some zettel identifier
; Number
: Specify the amount of zettel identifier you are planning to use.
  Minimum size is 100.
  If you need more than 10.000, your justification will contain more words.

=== Reserved Zettel Identifier

|= From | To | Description
| 00000000000000 | 0000000000000 | This is an invalid zettel identifier
| 00000000000001 | 0000009999999 | [[Predefined zettel|00001005090000]]
| 00000100000000 | 0000019999999 | This [[Zettelstore manual|00001000000000]]
| 00000200000000 | 0000899999999 | Reserved for future use
| 00009000000000 | 0000999999999 | Reserved for applications

This list may change in the future.

==== External Applications
|= From | To | Description
| 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow
| 00009000002000 | 00009000002999 | [[Zettel Blog|https://zettelstore.de/contrib]], an application to collect and transform zettel into a blog

Changes to docs/manual/00001007000000.zettel.

1
2

3
4
5


6
7
8
9
10
11


12
13
14


15
16
17

18
19
20
21
22

23
24

25

26
27
28

29
30
31
32
33
34
35

36



1
2
3
4
5

6
7
8
9
10
11


12
13
14


15
16
17
18

19
20
21
22
23

24
25

26
27
28
29
30

31
32
33
34
35
36
37
38
39

40
41
42


+


-
+
+




-
-
+
+

-
-
+
+


-
+




-
+

-
+

+


-
+







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

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

Zettelmark supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer.
Zettelmark can be much easier parsed / consumed by a software compared to other markup languages.
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|https://commonmark.org/]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation.
Zettelmark follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation.
[[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation.
Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation.

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

Zettelmarkup is a rich markup language, but it focusses on relatively short zettel content.
Zettelmarkup is a rich markup language, but it focuses on relatively short zettel content.
It allows embedding other content, simple tables, quotations, description lists, and images.
It provides a broad range of inline formatting, including //emphasized//{-}, **strong**{-}, __underlined__, ;;small;;, ~~deleted~~{-} and __inserted__{-} text.
It provides a broad range of inline formatting, including __emphasized__, **strong**, ~~deleted~~{-} and >>inserted>> text.
Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys.
Zettelmarkup allows to include content from other zettel and to embed the result of a search query.

Zettelmarkup might be seen as a proprietary markup language.
But if you want to use Markdown/CommonMark and you need support for footnotes, you'll end up with a proprietary extension.
But if you want to use [[Markdown/CommonMark|00001008010000]] and you need support for footnotes or tables, you'll end up with proprietary extensions.
However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds.

* [[General principles|00001007010000]]
* [[Basic definitions|00001007020000]]
* [[Block-structured elements|00001007030000]]
* [[Inline-structured element|00001007040000]]
* [[Attributes|00001007050000]]
* [[Query expressions|00001007700000]]
* [[Summary of formatting characters|00001007060000]]
* [[Summary of formatting characters|00001007800000]]
* [[Tutorial|00001007900000]]
* [[Cheat Sheet|00001007990000]]

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

6
7

8
9

10
11
12


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

28
29

30
31
32
33
34


35
36
37
38
39
40
41
42
43


44
45
46
47
48
49
50
51
52


+


-
+

-
+

-
+


-
-
+
+













-
+

-
+




-
-
+
+







-
-
+
+







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

Any document can be thought as a sequence of paragraphs and other blocks-structural elements (""blocks""), such as headings, lists, quotations, and code blocks.
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-structural elements (""inlines""), such as text, links, emphasized text, and images.
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 begins at the first position of a line, but may need one or more character, plus a space character.
Table blocks begins at the first position of a line with the character ""``|``"".
List blocks also begin at the first position of a line, but may need one or more identical character, plus a space character.
[[Table blocks|00001007031000]] begin at the first position of a line with the character ""``|``"".
Non-list blocks are either fully specified on that line or they span multiple lines and are delimited with the same three or more character.
It depends on the block kind, whether blocks are specified on one line or on at least two lines.

If a line does not begin with an explicit block element. the line is treated as a (implicit) paragraph block element that contains inline elements.
This paragraph ends when a block element is detected at the beginning of a next line or when an empty line occurs.
Some blocks may also contain inline elements, e.g. a heading.

Inline elements mostly begins with two non-space, often identical characters.
With some exceptions, two identical non-space characters begins a formatting range that is ended with the same two characters.

Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"".
A link is given with ``[[...]]``{=zmk}, an images with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}.
An inline comment, beginning with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins.
The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If placed at the end of non-space text.].
The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If put at the end of non-space text.].

Some inline elements do not follow the rule of two identical character, especially to specify footnotes, citation keys, and local marks.
Some inline elements do not follow the rule of two identical character, especially to specify [[footnotes|00001007040330]], [[citation keys|00001007040340]], and local marks.
These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``"").

One inline element that does not begin with two characters is the ""entity"".
It allows to specify any Unicode character.
The specification of that character is placed between an ampersand character and a semicolon: ``&...;``{=zmk}.
For exmple, an ""n-dash"" could also be specified as ``&ndash;``{==zmk}.
The specification of that character is put between an ampersand character and a semicolon: ``&...;``{=zmk}.
For example, an ""n-dash"" could also be specified as ``&ndash;``{==zmk}.

The backslash character (""``\\``"") possibly gives the next character a special meaning.
This allows to resolve some left ambiguities.
For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}.
An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}.
To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified.

Many block and inline elements can be refined by additional attributes.
Attributes resemble roughly HTML attributes and are placed near the corresponding elements by using the syntax ``{...}``{=zmk}.
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.
* 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.

Changes to docs/manual/00001007020000.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: 00001007020000
title: Zettelmarkup: Basic Definitions
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
modified: 20220218130713

Every zettelmark content consists of a sequence of Unicode codepoints.
Unicode codepoints are called in the following as **character**s.
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.

; Line
A **line** is a sequence of characters, except newline (''U+000A'') and carraige return (''U+000D''), followed by a line ending sequence or the end of content.

A **line ending** is either a newline not followed by a carriage return, a newline followed by a carriage return, or a carriage return.
Different line can be finalized by different line endings.

An **empty line** is an empty sequence of characters, followed by a line ending or the end of content.

The **space** character is the Unicode codepoint ''U+0020''.
: A __line__ is a sequence of characters, except newline (U+000A) and carriage return (U+000D), followed by a line ending sequence or the end of content.
; Line ending
: A __line ending__ is either a newline not followed by a carriage return, a newline followed by a carriage return, or a carriage return.
  Different line can be finalized by different line endings.
; Empty line
: An __empty line__ is an empty sequence of characters, followed by a line ending or the end of content.
; Space character
: The __space character__ is the Unicode code-point U+0020.

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

2
3
4
5

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




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

-
+
+


-
+



















+











-
-
-
-
+
+
+
+
+
+
+







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

Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists

In Zettelmarkup, lists themselves are not specified, but list items.
A sequence of list items is considered as a list.

[[Description lists|00001007030100]] contain two different item types: the term to be described and the description itself.
These cannot be combined with other lists.

Ordered lists, unordered lists, and quotation lists can be combined into [[nested lists|00001007030200]].

=== One-line blocks

* [[Headings|00001007030300]] allow to structure the content of a zettel.
* The [[horizontal rule|00001007030400]] signals a thematic break
* A [[transclusion|00001007031100]] embeds the content of one zettel into another.

=== Line-range blocks

This kind of blocks encompass at least two lines.
To be useful, they encompass more lines.
They begin with at least three identical characters at the first position of the beginning line.
They end at the line, that contains at least the same number of these identical characters, beginning at the first position of that line.
This allows line-range blocks to be nested.
Additionally, all other blocks elements are allowed in line-range blocks.

* [[Verbatim blocks|00001007030500]] do not interpret their content,
* [[Quotation blocks|00001007030600]] specify a block-length quotation,
* [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important
* [[Region blocks|00001007030800]] just mark a region, e.g. for common formatting
* [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered
* [[Quotation blocks|00001007030600]] specify a block-length quotations,
* [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important,
* [[Region blocks|00001007030800]] just mark regions, e.g. for common formatting,
* [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered.
* [[Evaluation blocks|00001007031300]] specify some content to be evaluated by either Zettelstore or external software.
* [[Math-mode blocks|00001007031400]] can be used to enter mathematical formulas / equations.
* [[Inline-Zettel blocks|00001007031200]] provide a mechanism to specify zettel content with a new syntax without creating a new zettel.

=== Tables

Similar to lists are tables not specified explicitly.
A sequence of table rows is considered a [[table|00001007031000]].
A table row itself is a sequence of table cells.

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


+


-
+




-
+


-
+







id: 00001007030100
title: Zettelmarkup: Description Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
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.
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.

The description of a term is given with one colon (""'':''"", ''U+003A'') at the first position, followed by a space character and the description itself, specified as a sequence of inline elements.
The description of a term is given with one colon (""'':''"", U+003A) at the first position, followed by a space character and the description itself, specified as a sequence of [[inline elements|00001007040000]].
Similar to terms, following lines can also be part of the actual description, if they begin at each line with exactly two space characters.

In contrast to terms, the actual descriptions are merged into a paragraph.
This is because, an actual description can contain more than one paragraph.
As usual, paragraphs are separated by an empty line.
Every following paragraph of an actual description must be indented by two space characters.

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

6
7
8
9


10
11
12

13
14
15
16
17
18
19
20


+


-
+



-
-
+
+

-
+







id: 00001007030200
title: Zettelmarkup: Nested Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
modified: 20220218133902

There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists.

Ordered lists are specified with the number sign (""''#''"", ''U+0023''), unordered lists use the asterisk (""''*''"", ''U+002A''), and quotation lists are specified with the greater-than sing (""''>''"", ''U+003E'').
Let's call these three characters //list characters//.
Ordered lists are specified with the number sign (""''#''"", U+0023), unordered lists use the asterisk (""''*''"", U+002A), and quotation lists are specified with the greater-than sing (""''>''"", U+003E).
Let's call these three characters __list characters__.

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.
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.
86
87
88
89
90
91
92
93
94


95
96
97
98
99
100
101
87
88
89
90
91
92
93


94
95
96
97
98
99
100
101
102







-
-
+
+







*# A.3
* B

* C
:::

Please note that two lists cannot be separated by an empty line.
Instead you should place a horizonal rule (""thematic break"") between them.
You could also use a mark element or a hard line break to separate the two lists:
Instead you should put a horizontal rule (""thematic break"") between them.
You could also use a [[mark element|00001007040350]] or a hard line break to separate the two lists:
```zmk
# One
# Two
[!sep]
# Uno
# Due
---

Changes to docs/manual/00001007030300.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: 00001007030300
title: Zettelmarkup: Headings
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
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.
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
=== Level 1 Heading
==== Level 2 Heading
===== Level 3 Heading
====== Level 4 Heading
======= Level 5 Heading
28
29
30
31
32
33
34
35

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

36
37
38
39
40
41
42







-
+







=== Notes

The heading level is translated to a HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==<h2>Level 1 Heading</h2>=={=html}.
The ==<h1>=={=html} tag is rendered for the zettel title.

This syntax is often used in a similar way in wiki implementation.
However, trailing equal signs are //not//{-} removed, they are part of the heading text.
However, trailing equal signs are __not__ removed, they are part of the heading text.

If you use command line tools, you can easily create a draft table of contents with the command:

```sh
grep -h '^====* ' ZETTEL_ID.zettel
```

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

-
+
+


-
+
+

-
-
+
+

-
+

-
+





-
+







-
+




id: 00001007030400
title: Zettelmarkup: Horizontal Rule
title: Zettelmarkup: Horizontal Rules / Thematic Break
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20220825185533

To signal a thematic break, you can specify a horizonal rule.
This is done by entering at least three hyphen-minus characters (""''-''"", ''U+002D'') at the first position of a line.
To signal a thematic break, you can specify a horizontal rule.
This is done by entering at least three hyphen-minus characters (""''-''"", U+002D) at the first position of a line.
You can add some [[attributes|00001007050000]], although the horizontal rule does not support the default attribute.
Any other character in this line will be ignored
Any other characters in this line will be ignored.

If you do not enter the three hyphen-minus charachter at the very first position of a line, the are interpreted as [[inline elements|00001007040000]], typically as an ""en-dash" followed by a hyphen-minus.
If you do not enter the three hyphen-minus character at the very first position of a line, the are interpreted as [[inline elements|00001007040000]], typically as an ""en-dash" followed by a hyphen-minus.

Example:

```zmk
---
----{color=green}
----{.zs-deprecated}
-----
 --- inline
--- ignored
```
is rendered in HTML as
:::example
---
----{color=green}
----{.zs-deprecated}
-----
 --- inline
--- ignored
:::

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


+


-
+


-
-
+
+


-
+

-
+







id: 00001007030500
title: Zettelmarkup: Verbatim Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
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.].
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.
The verbatim block supports the default attribute: when given, all spaces in the text are rendered in HTML as open box characters (""''&#x2423;''"", ''U+2423'').
The verbatim block supports the default attribute: 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 (programming) language to support colourizing the text when rendered in HTML.
It will be interpreted as a ([[programming|00001007050200]]) language to support colorizing the text when rendered in HTML.

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 grave accent characters in the text that should not be interpreted.

For example:

Changes to docs/manual/00001007030600.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: 00001007030600
title: Zettelmarkup: Quotation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
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 seomeone, a quotation block is more appropriately.
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.
This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a quotation block, following the initiating characters.
The quotation does not support the default attribute, nor the generic attribute.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored

Text following the beginning line will 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 a quotation block within a quotation block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the less-than characters.
These will interpreted as some attribution text.

For example:

```zmk
<<<<
A quotation with an embedded quotation
<<<{style=color:green}
Embedded
<<< Embedded Author
<<<<
<<<< Quotation Author
```
will be rendered in HTML as:
:::example
<<<<
A quotation with an embedded quotation
<<<{style=color:green}
Embedded
<<< Embedded Author
<<<< Quotation Author
:::

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

6
7
8
9
10
11
12

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

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

46
47
48
49
50
51
52


+


-
+






-
+


















-
+













-
+






id: 00001007030700
title: Zettelmarkup: Verse Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
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.

This kind of line-range block begins with at least three quotation mark characters (""''"''"", ''U+0022'') at the first position of a line.
This kind of line-range block begins with at least three quotation mark characters (""''"''"", U+0022) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a verse block, following the initiating characters.
The verse block does not support the default attribute, nor the generic attribute.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored.

Text following the beginning line will 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 a verse block within a verse block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the quotation mark characters.
These will interpreted as some attribution text.

For example:

```zmk
""""
A verse block with
 an
  embedded
   verse block
"""{style=color:green}
"""{.zs-deprecated}
Embedded
  verse
block
""" Embedded Author
"""" Verse Author
```
will be rendered as:
:::example
""""
A verse block with
 an
  embedded
   verse block
"""{style=color:green}
"""{.zs-deprecated}
Embedded
  verse
block
""" Embedded Author
"""" Verse Author
:::

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


+


-
+






-
-
+
+


+







id: 00001007030800
title: Zettelmarkup: Region Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
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.

This kind of line-range block begins with at least three colon characters (""'':''"", ''U+003A'') at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.].
You can add some [[attributes|00001007050000]] on the beginning line of a verse block, following the initiating characters.
This kind of line-range block begins with at least three colon characters (""'':''"", U+003A) at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.].
You can add some [[attributes|00001007050000]] on the beginning line of a region block, following the initiating characters.
The region block does not support the default attribute, but it supports the generic attribute.
Some generic attributes, like ``=note``, ``=warning`` will be rendered special.
All other generic attributes are used as a CSS class definition.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored.

Text following the beginning line will 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 a region block within a region block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the colon characters.
These will interpreted as some attribution text.
63
64
65
66
67
68
69













65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







+
+
+
+
+
+
+
+
+
+
+
+
+
Generic attributes that are result in a special HTML rendering are:
* example
* note
* tip
* important
* caution
* warning

All other generic attribute values are rendered as a CSS class:
```zmk
:::abc
def
:::
```
is rendered as
::::example
:::abc
def
:::
::::

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
24


+


-
+
+





-
+


-
-
+
+







id: 00001007030900
title: Zettelmarkup: Comment Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20230807170858

Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted.
While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks begin with at least three percent sign characters (""''%''"", ''U+0025'') at the first position of a line.
Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to JSON, the comment block will not be ignored but it will output some JSON text.
Same for other renderers.
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.

For example:

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

6
7


8
9
10

11
12
13

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

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



55
56
57
58
59
60
61
62
63
64





-
+

-
-
+
+

-
+


-
+




















-
+



















-
-
-
+
+
+







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

Tables are used to show some data in a two-dimenensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering //table rows//.
Tables are used to show some data in a two-dimensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering __table rows__.
Therefore, a table can be seen as a sequence of table rows.
A table row is nothing as a sequence of //table cells//.
A table row is nothing as a sequence of __table cells__.
The length of a table is the number of table rows, the width of a table is the maximum length of its rows.

The first cell of a row must begin with the vertical bar character (""''|''"", ''U+007C'') at the first position of a line.
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.
Inside a cell, you can specify any [[inline elements|00001007040000]].

For example:
```zmk
| a1 | a2 | a3|
| b1 | b2 | b3
| c1 | c2
```
will be rendered in HTML as:
:::example
| a1 | a2 | a3|
| b1 | b2 | b3
| c1 | c2
:::

=== Header row
If any cell in the first row of a table contains an equal sing character (""''=''"", ''U+003D'') as the very first character, then this first row will be interpreted as a //table header// row.
If any cell in the first row of a table contains an equal sing character (""''=''"", U+003D) as the very first character, then this first row will be interpreted as a __table header__ row.

For example:
```zmk
| a1 | a2 |= a3|
| b1 | b2 | b3
| c1 | c2
```
will be rendered in HTML as:
:::example
| a1 | a2 |= a3|
| b1 | b2 | b3
| c1 | c2
:::

=== Column alignment
Inside a header row, you can specify the alignment of each header cell by a given character as the last character of a cell.
The alignment of a header cell determines the alignment of every cell in the same column.
The following characters specify the alignment:

* the colon character (""'':''"", ''U+003A'') forces a centered aligment,
* the less-than sign character (""''<''"", ''U+0060'') specifies an alignment to the left,
* the greater-than sign character (""''>''"", ''U+0062'') will produce right aligned cells.
* the colon character (""'':''"", U+003A) forces a centered alignment,
* the less-than sign character (""''<''"", U+0060) specifies an alignment to the left,
* the greater-than sign character (""''>''"", U+0062) will produce right aligned cells.

If no alignment character is given, a default alignment is used.

For example:
```zmk
|=Left<|Right>|Center:|Default
|123456|123456|123456|123456|
86
87
88
89
90
91
92
93
94


95
96

97
98
99
100


101
102
103
104
105
106


107
86
87
88
89
90
91
92


93
94
95

96
97
98


99
100
101
102
103
104


105
106
107







-
-
+
+

-
+


-
-
+
+




-
-
+
+

|=Left<|Right>|Center:|Default
|>R|:C|<L
|123456|123456|123456|123456|
|123|123|123|123
:::

=== Rows to be ignored
A line that begins with the sequence ''|%'' (vertical bar character (""''|''"", ''U+007C''), followed by a percent sign character (“%”, U+0025)) will be ignored.
This allows to specify a horizontal rule that is not rendered.
A line that begins with the sequence ''|%'' (vertical bar character (""''|''"", U+007C), followed by a percent sign character (“%”, U+0025)) will be ignored.
For example, this allows to specify a horizontal rule that is not rendered.
Such tables are emitted by some commands of the [[administrator console|00001004100000]].
For example, the command ``get-config place`` will emit
For example, the command ``get-config box`` will emit:
```
|=Key        | Value  | Description
|%-----------+--------+-----------------------------
| defdirtype | notify | Default directory place type
|%-----------+--------+---------------------------
| defdirtype | notify | Default directory box type
```
This is rendered in HTML as:
:::example
|=Key        | Value  | Description
|%-----------+--------+-----------------------------
| defdirtype | notify | Default directory place type
|%-----------+--------+---------------------------
| defdirtype | notify | Default directory box type
:::

Added docs/manual/00001007031100.zettel.


























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007031100
title: Zettelmarkup: Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220131151022
modified: 20220922155031

A transclusion allows to include the content of other zettel into the current zettel.

The transclusion specification begins with three consecutive left curly bracket characters (""''{''"", U+007B) at the first position of a line and ends with three consecutive right curly bracket characters (""''}''"", U+007D).
The curly brackets delimit either a [[zettel identifier|00001006050000]] or a searched zettel list.
You can add some [[attributes|00001007050000]], although a transclusion does not support the default attribute.
Any other characters in this line will be ignored.

This leads to three variants of transclusions:
# Transclusion of the content of another zettel into the current zettel.
  This is done if you specify a zettel identifier, and is called __zettel transclusion__.
# Transclusion of the list of zettel references that satisfy a [[query expression|00001007700000]].
  This is called __query transclusion__.
# Transclusion of the content of an image, referenced by a [[hosted or based|00001007040310#link-specifications]] link / URL.

The first two variants are described on separate zettel:
* [[Zettel transclusion|00001007031110]]
* [[Query transclusion|00001007031140]]

Added docs/manual/00001007031110.zettel.













































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

A zettel transclusion is specified by the following sequence, starting at the first position in a line: ''{{{zettel-identifier}}}''.

When evaluated, the referenced zettel is read.
If it contains some transclusions itself, these will be expanded, recursively.
When a recursion is detected, expansion does not take place.
Instead an error message replaces the transclude specification.

An error message is also given, if the zettel cannot be read or if too many transclusions are made.
The maximum number of transclusion can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel.

If everything went well, the referenced, expanded zettel will replace the transclusion element.

For example, to include the text of the Zettel titled ""Zettel identifier"", just specify its identifier [[''00001006050000''|00001006050000]] in the transclude element:
```zmk
{{{00001006050000}}}
```
This will result in:
:::example
{{{00001006050000}}}
:::

Please note: if the referenced zettel is changed, all transclusions will also change.

This allows, for example, to create a bigger document just by transcluding smaller zettel.

In addition, if a zettel __z__ transcludes a zettel __t__, but the current user is not allowed to view zettel __t__ (but zettel __z__), then the transclusion will not take place.
To the current user, it seems that there was no transclusion in zettel __z__.
This allows to create a zettel with content that seems to be changed, depending on the authorization of the current user.

---
Any [[attributes|00001007050000]] added to the transclusion will set/overwrite the appropriate metadata of the included zettel.
Of course, this applies only to thoes attribtues, which have a valid name for a metadata key.
This allows to control the evaluation of the included zettel, especially for zettel containing a diagram description.

=== See also
[[Inline-mode transclusion|00001007040324]] does not work at the paragraph / block level, but is used for [[inline-structured elements|00001007040000]].

Added docs/manual/00001007031140.zettel.






































































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

A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''.
The line must literally start with the sequence ''{{{query:''.
Everything after this prefix is interpreted as a [[query expression|00001007700000]].

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.

For example, to include the list of all zettel with the [[tags|00001006020000#tags]] ""#search"", ordered by title specify the following query transclude element:
```zmk
{{{query:tags:#search ORDER title}}}
```
This will result in:
:::example
{{{query:tags:#search ORDER title}}}
:::

For example, this allows to create a dynamic list of zettel inside a zettel, maybe to provide some introductory text followed by a list of child zettel.

The query will deliver only those zettel, which the current user is allowed to read.

In the above example, the action list is empty.
This leads to the described list of zettel.

The following actions are supported, parameter and aggregate actions:
; ''N'' (or any word that starts with ""''N''"" (parameter)
: The resulting list will be a numbered list.
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''TITLE'' (parameter)
: All words following ''TITLE'' are joined together to form a title.
  It is used for the ''ATOM'' and ''RSS'' action.
; ''ATOM'' (aggregate)
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'', ''REINDEX'' (aggregate)
: Will be ignored.
  These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful).
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case[^Except if the key name collides with one of the above names. In this case use at least one lower case letter.].

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

Example:
```zmk
{{{query:tags:#search | tags}}}
```
This is a tag cloud of all tags that are used together with the tag #search:
:::example
{{{query:tags:#search | tags}}}
:::

Added docs/manual/00001007031200.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: 00001007031200
title: Zettelmarkup: Inline-Zettel Block
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220201142439
modified: 20221018121251

An inline-zettel block allows to specify some content with another syntax without creating a new zettel.
This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup.
Another example is to specify HTML code to use it for some kind of web front-end framework.

As all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line.
For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The inline-zettel block uses the attribute key ""syntax"" to specify the [[syntax|00001008000000]] of the inline-zettel.
Alternatively, you can use the generic attribute to specify the syntax value.
If no value is provided, ""[[text|00001008000000#text]]"" is assumed.

Any other character in this first 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 at-sign characters given at the beginning line.
This allows to enter some at-sign characters in the text that should not be interpreted at this level.

Some examples:
```zmk
@@@markdown
A link to [this](00001007031200) zettel.
@@@
```
will be rendered as:
:::example
@@@markdown
A link to [this](00001007031200) zettel.
@@@
:::

If you have set [[''insecure-html''|00001004010000#insecure-html]] to the value ""zettelmarkup"", the following markup is not ignored:

```zmk
@@@html
<h1>H1 Heading</h1>
Alea iacta est
@@@
```
will render a section heading of level 1, which is not allowed within Zettelmarkup:
:::example
@@@html
<h1>H1 Heading</h1>
Alea iacta est
@@@
:::
:::note
Please note: some HTML code will not be fully rendered because of possible security implications.
This include HTML lines that contain a ''<script>'' tag or an ''<iframe>'' tag.
:::
Of course, you do not need to switch the syntax and you are allowed to nest inline-zettel blocks:
```zmk
@@@@zmk
1st level inline
@@@zmk
2nd level inline

Transclusion of zettel to enable authentication:
{{{00001010040100}}}
@@@
@@@@
```
will result in the following HTML output:
:::example
@@@@zmk
1st level inline
@@@zmk
2nd level inline

Transclusion of zettel to enable authentication:
{{{00001010040100}}}
@@@
@@@@
:::

Added docs/manual/00001007031300.zettel.




































































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

Evaluation blocks are used to enter text that could be evaluated by either Zettelstore or external software.
They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".]
The main reason for an evaluation block is to be used with external software via the [[sz encoding|00001012920516]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some tilde characters in the text that should not be interpreted.

For example:
`````zmk
~~~~
~~~
~~~~
`````
will be rendered in HTML as:
:::example
~~~~
~~~
~~~~
:::

`````zmk
~~~{-}
This is  some
text with no 
  real sense.
~~~~
`````
will be rendered as:
:::example
~~~{-}
This is  some
text with no 
  real sense.
~~~~
:::

`````zmk
~~~draw
+---+     +---+
| A | --> | B |
+---+     +---+
~~~
`````
will be rendered as:
:::example
~~~draw
+---+     +---+
| A | --> | B |
+---+     +---+
~~~
:::

Added docs/manual/00001007031400.zettel.












































































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

Math-mode blocks are used to enter mathematical formulas / equations in a display style mode.
Similar to a [[evaluation blocks|00001007031300]], the block content will be interpreted by either Zettelstore or an external software.
They begin with at least three dollar sign characters (""''$''"", U+0024) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax.
Not all syntax values are supported by Zettelstore.[^Currently: none.]
External software might support several values via the [[sz encoding|00001012920516]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some dollar-sign characters in the text that should not be interpreted.

For example:
`````zmk
$$$$
$$$
$$$$
`````
will be rendered in HTML as:
:::example
$$$$
$$$
$$$$
:::

`````zmk
$$${-}
This is  some
text with no 
  real sense.
$$$$
`````
will be rendered as:
:::example
$$${-}
This is  some
text with no 
  real sense.
$$$$
:::

In the future, Zettelstore might somehow support mathematical formulae with a $$\TeX$$-like syntax.
Until then,
`````zmk
$$$
\begin{align*}
  f(x) &= x^2\\
  g(x) &= \frac{1}{x}\\
  F(x) &= \int^a_b \frac{1}{3}x^3
\end{align*}
$$$
`````
is rendered as:
:::example
$$$
\begin{align*}
  f(x) &= x^2\\
  g(x) &= \frac{1}{x}\\
  F(x) &= \int^a_b \frac{1}{3}x^3
\end{align*}
$$$
:::

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

6
7
8
9
10
11
12
13




14
15
16
17
18



19
20
21
22







23
24
25
26
27
28
29
30
31


32
33
34
35
36



37
38
39
40

41




42
43
44
45
46


47
48
49
50
51
52
53

54
55
56
57
58
59
60
61

62
63
64
65





+


-
+
+






-
-
-
-
+
+
+
+

-
-
-
+
+
+

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


-
-
+
+



-
-
-
+
+
+

-
+
-
-
-
-





-
-
+
+





-
+


+
+
+


-
+



-
-
-
id: 00001007040000
title: Zettelmarkup: Inline-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20220920143243

Most characters you type is concerned with inline-structured elements.
The content of a zettel contains is many cases just ordinary text, lightly formatted.
Inline-structured elements allow to format your text and add some helpful links or images.
Sometimes, you want to enter characters that have no representation on your keyboard.

=== Text formatting
Every [[text formatting|00001007040100]] element begins with two same characters at the beginning.
It lasts until the same two characters occurred the second time.
Some of these elements explicitly support [[attributes|00001007050000]].
; Text formatting
: Every [[text formatting|00001007040100]] element begins with two same characters at the beginning.
  It lasts until the same two characters occurred the second time.
  Some of these elements explicitly support [[attributes|00001007050000]].

=== Literal-like formatting
Sometime you want to render the text as it is.
This is the core motivation of [[literal-like formatting|00001007040200]].
; Literal-like formatting
: Sometime you want to enter the text as it is.
: This is the core motivation of [[literal-like formatting|00001007040200]].

=== Reference-like text
You can reference other zettel and (external) material within one zettel.
This kind of reference may be a link, or an images that is display inline when the zettel is rendered.
Footnotes sometimes factor out some useful text that hinders the flow of reading text.
Internal marks allow to reference something within a zettel.
An important aspect of all knowledge work is to reference others work, e.g. with citation keys.
All these elements can be subsumed under [[reference-like text|00001007040300]].
; Reference-like text
: You can reference other zettel and (external) material within one zettel.
  This kind of reference may be a link, or an images that is display inline when the zettel is rendered.
  Footnotes sometimes factor out some useful text that hinders the flow of reading text.
  Internal marks allow to reference something within a zettel.
  An important aspect of all knowledge work is to reference others work, e.g. with citation keys.
  All these elements can be subsumed under [[reference-like text|00001007040300]].

=== Other inline elements
==== Comments
A comment begins with two consecutive percent sign characters (""''%''"", ''U+0025'').
==== Comment
A comment begins with two consecutive percent sign characters (""''%''"", U+0025).
It ends at the end of the line where it begins.

==== Backslash
The backslash character (""''\\''"", ''U+005C'') gives the next character another meaning.
* If a space character follows, it is converted in a non-breaking space (''U+00A0'').
* If a line ending follows the backslash character, the line break is converted from a //soft break// into a //hard break//.
The backslash character (""''\\''"", U+005C) gives the next character another meaning.
* If a space character follows, it is converted in a non-breaking space (U+00A0).
* If a line ending follows the backslash character, the line break is converted from a __soft break__ into a __hard break__.
* Every other character is taken as itself, but without the interpretation of a Zettelmarkup element.
  For example, if you want to enter a ""'']''"" into a footnote text, you should escape it with a backslash.
  For example, if you want to enter a ""'']''"" into a [[footnote text|00001007040330]], you should escape it with a backslash.

==== Tag
Any text that begins with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an //inline tag//.
They will be considered equivalent to tags in metadata.

==== Entities & more
Sometimes it is not easy to enter special characters.
If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name.

Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B'').
If you know the HTML name of the character you want to enter, place it between these two character.
Regardless which method you use, an entity always begins with an ampersand character (""''&''"", U+0026) and ends with a semicolon character (""'';''"", U+003B).
If you know the HTML name of the character you want to enter, put it between these two character.
Example: ``&amp;`` is rendered as ::&amp;::{=example}.

If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10.
Example: ``&#38;`` is rendered in HTML as ::&#38;::{=example}.

You also can enter its numeric code point as a hex number, if you place the letter ""x"" after the numeric sign character.
You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character.
Example: ``&#x26;`` is rendered in HTML as ::&#x26;::{=example}.

According to the [[HTML Standard|https://html.spec.whatwg.org/multipage/syntax.html#character-references]], some numeric code points are not allowed.
These are all code point below the numeric value 32 (decimal) or 0x20 (hex) and all code points for [[noncharacter|https://infra.spec.whatwg.org/#noncharacter]] values.

Since some Unicode character are used quite often, a special notation is introduced for them:

* Two consecutive hyphen-minus characters result in an //en-dash// character.
* Two consecutive hyphen-minus characters result in an __en-dash__ character.
  It is typically used in numeric ranges.
  ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}.
  Alternative specifications are: ``&ndash;``, ``&x8211``, and ``&#x2013``.
* Three consecutive full stop characters (""''.''"", ''U+002E'') after a space result in an horizontal ellipsis character.
  ``to be continued ... later`` will be rendered in HTML as: ::to be continued, ... later::{=example}.
  Alternative specifications are: ``&hellip;``, ``&x8230``, and ``&#x2026``.

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





1
2
3
4
5

6
7
8
9
10
11
12
13
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: 00001007040100
title: Zettelmarkup: Text Formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20231113191353

Text formatting is the way to make your text visually different.
Every text formatting element begins with two same characters.
It ends when these two same characters occur the second time.
It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character.

Text formatting can be nested, up to a reasonable limit.

The following characters begin a text formatting:

* The slash character (""''/''"", ''U+002F'') emphasizes its text. Often, such text is rendered in italics. If the default attribute is specified, the emphasized text is not just rendered as such, but also internally marked as emphasized.
** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}.
* The low line character (""''_''"", U+005F) emphasizes its text.
** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}.
** Example: ``abc //def//{-} ghi`` is rendered in HTML as: ::abc //def//{-} ghi::{=example}.
* The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text. The text is often rendered in bold. Again, the default attribute will force a explicit semantic meaning of strong emphasizing.
* The asterisk character (""''*''"", U+002A) strongly emphasized its enclosed text.
** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}.
** Example: ``abc **def**{-} ghi`` is rendered in HTML as: ::abc **def**{-} ghi::{=example}.
* The low line character (""''_''"", ''U+005F'') produces underlined text. If the default attribute was specified, it is semantically rendered as inserted text.
** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}.
* The greater-than sign character (""''>''"", U+003E) marks text as inserted.
** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}.
** Example: ``abc __def__{-} ghi`` is rendered in HTML as: ::abc __def__{-} ghi::{=example}.
* Similar, the tilde character (""''~''"", ''U+007E'') produces text that is strike-through. If the default attribute was specified, it is semantically rendered as deleted text.
* Similar, the tilde character (""''~''"", U+007E) marks deleted text.
** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}.
** Example: ``abc ~~def~~{-} ghi`` is rendered in HTML as: ::abc ~~def~~{-} ghi::{=example}.
* The apostrophe character (""''\'''"", ''U+0027'') renders text in mono-space / fixed font width.
** Example: ``abc ''def'' ghi`` is rendered in HTML as: ::abc ''def'' ghi::{=example}.
* The circumflex accent character (""''^''"", ''U+005E'') allows to enter superscripted text.
* 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 subscripted text.
* The comma character (""'',''"", U+002C) produces sub-scripted text.
** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}.
* The less-than sign character (""''<''"", ''U+003C'') marks an inline quotation.
** Example: ``<<To be or not<<`` is rendered in HTML as: ::<<To be or not<<::{=example}.
* The quotation mark character (""''"''"", ''U+0022'') produces the right typographic quotation marks according to the [[specified language|00001007050100]].
* 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 semicolon character (""'';''"", ''U+003B'') specifies text that should be rendered with small characters.
** 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::{style=color:green} ghi`` is rendered in HTML as: abc ::def::{style=color:green} ghi.
* 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
13
14
15
16


17
18
19
20


21
22
23
24
25
26
27
28
29
30
31
32


33
34
35
36


37
38
39
40
41

42
43
44
45
46
47

































1
2
3
4
5

6
7
8
9
10
11
12
13
14


15
16
17
18


19
20
21
22
23
24
25
26
27
28
29
30


31
32
33
34


35
36
37
38
39
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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: 00001007040200
title: Zettelmarkup: Literal-like formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20210525121114
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.

=== Literal text
Literal text somehow relates to [[verbatim blocks|00001007030500]]: their content should not be interpreted further, but may be rendered special.
It is specified by two grave accent characters (""''`''"", ''U+0060''), followed by the text, followed by again two grave accent characters, optionally followed by an [[attribute|00001007050000]] specification.
Similar to the verbatim block, the literal element allows also a modifier letter grave accent (""''ˋ''"", ''U+02CB'') as an alternative to the grave accent character[^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.].
It is specified by two grave accent characters (""''`''"", U+0060), followed by the text, followed by again two grave accent characters, optionally followed by an [[attribute|00001007050000]] specification.
Similar to the verbatim block, the literal element allows also a modifier letter grave accent (""''ˋ''"", U+02CB) as an alternative to the grave accent character[^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.].
However, all four characters must be the same.

The literal element supports the default attribute: when given, all spaces in the text are rendered in HTML as open box characters (""''&#x2423;''"", ''U+2423'').
The use of a generic attribute allwos to specify a (programming) language that controls syntax colouring when rendered in HTML.
The literal element supports the default attribute: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
The use of a generic attribute allows to specify a ([[programming|00001007050200]]) language that controls syntax coloring when rendered in HTML.

If you want to specify a grave accent character in the text, either use modifier grave accent characters as delimiters for the element, or put a backslash character before the grave accent character you want to use inside the element.
If you want to enter a backslash character, you need to enter two of these.

Examples:
* ``\`\`abc def\`\``` is rendered in HTML as ::``abc def``::{=example}.
* ``\`\`abc def\`\`{-}`` is rendered in HTML as ::``abc def``{-}::{=example}.
* ``\`\`abc\\\`def\`\``` is rendered in HTML as ::``abc\`def``::{=example}.
* ``\`\`abc\\\\def\`\``` is rendered in HTML as ::``abc\\def``::{=example}.

=== Keyboard input
To mark text as input into a computer program, delimit your text with two plus sign characters (""''+''"", ''U+002B'') on each side.
=== Computer input
To mark text as input into a computer program, delimit your text with two apostrophe characters (""''\'''"", U+0027) on each side.

Example:
* ``++STRG-C++`` renders in HTML as ::++STRG-C++::{=example}.
* ``++STRG C++{-}`` renders in HTML as ::++STRG C++{-}::{=example}.
* ``''STRG-C''`` renders in HTML as ::''STRG-C''::{=example}.
* ``''STRG C''{-}`` renders in HTML as ::''STRG C''{-}::{=example}.

Attributes can be specified, the default attribute has the same semantic as for literal text.

=== Computer output
To mark text as output from a computer program, delimit your text with two equal sign characters (""''=''"", ''U+003D'') on each side.
To mark text as output from a computer program, delimit your text with two equal sign characters (""''=''"", U+003D) on each side.

Examples:
* ``==The result is: 42==`` renders in HTML as ::==The result is: 42==::{=example}.
* ``==The result is: 42=={-}`` renders in HTML as ::==The result is: 42=={-}::{=example}.

Attributes can be specified, the default attribute has the same semantic as for literal text.

=== Inline-zettel snippet
To specify an inline snippet in a different [[syntax|00001008000000]], delimit your text with two at-sign characters (""''@''"", U+0040) on each side.

You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use.
Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value.
If no value is provided, ""[[text|00001008000000#text]]"" is assumed.

Examples:
* ``A @@-->@@ B`` renders in HTML as ::A @@-->@@ B::{=example}.
* ``@@<small>@@{=html}Small@@</small>@@{=html}`` renders in HTML as ::@@<small>@@{=html}Small@@</small>@@{=html}::{=example}.

To some degree, an inline-zettel snippet is the @@<small>@@{=html}smaller@@</small>@@{=html} sibling of the [[inline-zettel block|00001007031200]].
For HTML syntax, the same rules apply.

=== Math mode / $$\TeX$$ input
This allows to enter text, that is typically interpreted by $$\TeX$$ or similar software.
The main difference to all other literal-like formatting above is that the backslash character (""''\\''"", U+005C) has no special meaning.
Therefore it is well suited the enter text with a lot of backslash characters.

Math mode text is delimited with two dollar signs (""''$''"", U+0024) on each side.

You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use.
Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value.
If no syntax value is provided, math mode text roughly corresponds to literal text.

Currently, Zettelstore does not support any syntax.
This will probably change.
However, external software might support specific syntax value, like ""tex"", ""latex"", ""mathjax"", ""itex"", or ""webtex"".
Even an empty syntax value might be supported.

Example:
* ``Happy $$\\TeX$$!``is rendered as ::Happy $$\TeX$$!::{=example}

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


11
12





13
14
15























































































+




-
-
+
+
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001007040300
title: Zettelmarkup: Reference-like text
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20210810172531

An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material.

There are several kinds of references that are allowed in Zettelmarkup:
* Links to other zettel.
* Links to (external material).
* [[Links to other zettel or to (external) material|00001007040310]]
* [[Embedded zettel or (external) material|00001007040320]] (""inline transclusion"")
* Embed images that are stored within your Zettelstore.
* Embed external images.
* Reference via a footnote.
* Reference via a citation key.
* Put a mark within your zettel that you can reference later with a link.
* [[Reference via a footnote|00001007040330]]
* [[Reference via a citation key|00001007040340]]
* Put a [[mark|00001007040350]] within your zettel that you can reference later with a link.

=== Links
There are two kinds of links, regardless of links to (internal) other zettel or to (external) material.
Both kinds begin with two consecutive left square bracket characters (""''[''"", ''U+005B'') and ends with two consecutive right square bracket characters (""'']''"", ''U+005D'').

The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", ''U+007C''): ``[[text|linkspecification]]``.

The second form just provides a link specification between the square brackets.
Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``.

The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]].
To reference some content within a zettel, you can append a number sign character (""''#''"", ''U+0023'') and the name of the mark to the zettel identifier.
The resulting reference is called ""zettel reference"".

To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].
If the URL begins with the slash character (""/"", ''U+002F''), or if it begins with ""./"" or with ""../"", i.e. without scheme, user info, and host name, the reference will be treated as a ""local reference"", otherwise as an ""external reference"".
If the URL begins with two slash characters, it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]].

The text in the second form is just a sequence of inline elements.

=== Images
To some degree, an image specification is conceptually not too far away from a link specification.
Both contain a link specification and optionally some text.
In contrast to a link, the link specification of an image must resolve to actual graphical image data.
That data is read when rendered as HTML, and is embedded inside the zettel as an inline image.

An image specification begins with two consecutive left curly bracket characters (""''{''"", ''U+007B'') and ends with two consecutive right curly bracket characters (""''}''"", ''U+007D'').
The curly brackets delimits either a link specification or some text, a vertical bar character and the link specification, similar to a link.

One difference to a link: if the text was not given, an empty string is assumed.

The link specification must reference a graphical image representation if the image is about to be rendered.
Supported formats are:

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

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

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

Examples:
* ``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}`` is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}.
* The above image is also a placeholder for a non-existent image:
** ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}.
** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}.

=== Footnotes

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

=== Citation key

A citation key references some external material that is part of a bibliografical 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.
The key is typically a sequence of letters and digits.
If a comma character (""'',''"", ''U+002C'') or a vertical bar character is given, the following is interpreted as inline elements.
A right square bracket ends the text and the citation key element.

=== Mark
A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a bigger-sized zettel[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called //transclusion//.].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", ''U+0021'').
Now the optional mark name follows.
It is a (possibly empty) sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low-line character (""''_''"", ''U+005F'').
The mark element ends with a right square bracket.

Examples:
* ``[!]`` is a mark without a name, the empty mark.
* ``[!mark]`` is a mark with the name ""mark"".

Added docs/manual/00001007040310.zettel.







































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007040310
title: Zettelmarkup: Links
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20221024173849

There are two kinds of links, regardless of links to (internal) other zettel or to (external) material.
Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and ends with two consecutive right square bracket characters (""'']''"", U+005D).
If the content starts with more than two left square bracket characters, all but the last two will be treated as text.

The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``.
The text is a sequence of [[inline elements|00001007040000]].
However, it should not contain links itself.

The second form just provides a link specification between the square brackets.
Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``.

=== Link specifications
The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]].
To reference some content within a zettel, you can append a number sign character (""''#''"", U+0023) and the name of the mark to the zettel identifier.
The resulting reference is called ""zettel reference"".

If the link specification begins with the string ''query:'', the text following this string will be interpreted as a [[query expression|00001007700000]].
The resulting reference is called ""query reference"".
When this type of references is rendered, it will typically reference a list of all zettel that fulfills the query expression.

A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character,
will be interpreted as a local reference, called __hosted reference__.
Such references will be interpreted relative to the web server hosting the Zettelstore.

If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]].

To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].

=== Other topics
If the link references another zettel, and this zettel is not readable for the current user, because of a missing access rights, then only the associated text is presented.

Added docs/manual/00001007040320.zettel.





























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007040320
title: Zettelmarkup: Inline Embedding / Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20221024173926

To some degree, an specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]].
Both contain a reference specification and optionally some text.
In contrast to a link, the specification of embedded material must currently resolve to some kind of real content.
This content replaces the embed specification.

An embed specification begins with two consecutive left curly bracket characters (""''{''"", U+007B) and ends with two consecutive right curly bracket characters (""''}''"", U+007D).
The curly brackets delimits either a reference specification or some text, a vertical bar character and the link specification, similar to a link.
If the content starts with more than two left curly bracket characters, all but the last two will be treated as text.

One difference to a link: if the text was not given, an empty string is assumed.

The reference must point to some content, either zettel content or URL-referenced content.
If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored.
If the referenced zettel does not exist, or is not readable because of other reasons, a [[spinning emoji|00000000040001]] is presented as a visual hint:
 
Example: ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}.

There are two kind of content:
# [[image content|00001007040322]],
# [[textual content|00001007040324]].

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20221112111054

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

Supported formats are:

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

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

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

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

Added docs/manual/00001007040324.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: 00001007040324
title: Zettelmarkup: Inline-mode Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20231222164501

Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content.
For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]).

Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements.

First, the referenced zettel is read.
If it contains other transclusions, these will be expanded, recursively.
When an endless recursion is detected, expansion does not take place.
Instead an error message replaces the transclude specification.

The result of this (indirect) transclusion is searched for inline-structured elements.

* If only an [[zettel identifier|00001006050000]] was specified, the first top-level [[paragraph|00001007030000#paragraphs]] is used.
  Since a paragraph is basically a sequence of inline-structured elements, these elements will replace the transclude specification.

  Example: ``{{00010000000000}}`` (see [[00010000000000]]) is rendered as ::{{00010000000000}}::{=example}.

* If a fragment identifier was additionally specified, the element with the given fragment is searched:
** If it specifies a [[heading|00001007030300]], the next top-level paragraph is used.

   Example: ``{{00010000000000#reporting-errors}}`` is rendered as ::{{00010000000000#reporting-errors}}::{=example}.

** In case the fragment names a [[mark|00001007040350]], the inline-structured elements after the mark are used.
   Initial spaces and line breaks are ignored in this case.

   Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}.

** Just specifying the fragment identifier will reference something in the current page.
   This is not allowed, to prevent a possible endless recursion.

* If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered.

  Example: ``{{//z/00000000040001}}{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]].

Added docs/manual/00001007040330.zettel.













1
2
3
4
5
6
7
8
9
10
11
12
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007040330
title: Zettelmarkup: Footnotes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220218130100

A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", U+005E), followed by some text, and ends with a right square bracket.

Example:

``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}.

Added docs/manual/00001007040340.zettel.
















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007040340
title: Zettelmarkup: Citation Key
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220218133447

A citation key references some external material that is part of a bibliographical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given.
The key is typically a sequence of letters and digits.
If a comma character (""'',''"", U+002C) or a vertical bar character is given, the following is interpreted as [[inline elements|00001007040000]].
A right square bracket ends the text and the citation key element.

Added docs/manual/00001007040350.zettel.






















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007040350
title: Zettelmarkup: Mark
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220218133206

A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a zettel, either with a [[link|00001007040310]] or with an [[inline-mode transclusion|00001007040324]].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", U+0021).
Now the optional mark name follows.
It is a (possibly empty) sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", U+002D), or the low-line character (""''_''"", U+005F).
An optional text to be explicitly marked is introduced with a vertical bar character (""''|''"", U+007C), followed by some [[inline-structured elements|00001007040000]].
The mark element ends with a right square bracket.

Examples:
* ``[!]`` is a mark without a name, the empty mark.
* ``[!mark]`` is a mark with the name ""mark"".
* ``[!|some text]``is the empty mark with ""some text"".
* ``[!mark|some text]``is a mark with the name ""mark"" and with ""some text"" that is marked.

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

6
7
8
9
10
11

12
13
14
15
16
17
18

19
20


21
22
23
24
25
26

27
28
29






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


51
52
53


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83


84
85
86
87
88
89
90
91
92
93
94
95
96
97
98


99
100

101
102
103
104

105
106










107

108

109

110
111









112







113
114
115


+


-
+





-
+






-
+

-
-
+
+




-
+


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














-
-
+
+

-
-
+
+

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













-
-
+

-
+



-
+

-
-
-
-
-
-
-
-
-
-

-
+
-

-
+

-
-
-
-
-
-
-
-
-

-
-
-
-
-
-
-



id: 00001007050000
title: Zettelmarkup: Attributes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
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 raw text.
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.

Attributes are specified within curly brackets ``{...}``.
Of course, more than one attribute can be specified.
Attributes are separated by a sequence of space characters or by a comma character.

An attribute normally consists of an optional key and an optional value.
The key is a sequence of letters, digits, a hyphen-minus (""''-''"", ''U+002D'', and a low line / underscore (""''_''"", ''U+005D'').
The key is a sequence of letters, digits, a hyphen-minus (""''-''"", U+002D, and a low line / underscore (""''_''"", U+005D).
It can be empty.
The value is a sequence of any character, except space and the right curly bracket (""''}''"", ''U+007D'').
If the value must contain a space or the right curly bracket, the value can be specified within two quotation marks (""''"''"", ''U+0022'').
The value is a sequence of any character, except space and the right curly bracket (""''}''"", U+007D).
If the value must contain a space or the right curly bracket, the value can be specified within two quotation marks (""''"''"", U+0022).
Within the quotation marks, the backslash character functions as an escape character to specify the quotation mark (and the backslash character too).

Some examples:

* ``{key=value}`` sets the attribute //key// to value //value//.
* ``{key=value}`` sets the attribute __key__ to value __value__.
* ``{key="value with space"}`` sets the attribute to the given value.
* ``{key="value with quote \\" (and backslash \\\\)"}``
* ``{name}`` sets the attribute //name//. It has no corresponding value. It is
  equivalent to ``{name=}``.
* ``{=key}`` sets the //generic attribute//{-} to the given value. It is mostly
  used for modifying behaviour according to a programming language.
* ``{.key}`` sets the //class attribute//{-} to the given value. It is
  equivalent to ``{class=key}``. 
* ``{name}`` sets the attribute __name__.
  It has no corresponding value.
  It is equivalent to ``{name=}``.
* ``{=key}`` sets the __generic attribute__ to the given value.
  It is mostly used for modifying behavior according to a programming language.
* ``{.key}`` sets the __class attribute__ to the given value.
  It is equivalent to ``{class=key}``. 

In these examples, ``key`` must conform the the syntax of attribute keys, even if it is used as a value.

If a key is given more than once in an attribute, the values are concatenated (and separated by a space).

* ``{key=value1 key=value2}`` is the same as ``{key"value1 value2"}``.
* ``{key key}`` is the same as ``{key}``.
* ``{.class1 .class2}`` is equivalent to ``{class="class1 class2"}``.

This is not true for the generic attribute.
In ``{=key1 =key2}``, the first key is ignored.
Therefore it is equivalent to ``{=key2}``.

The key ""''-''"" (just hyphen-minus) is special.
It is called //default attribute//{-} and has a markup specific meaning.
For example, when used for raw text, it replaces the non-visible space with a visible representation:
It is called __default attribute__ and has a markup specific meaning.
For example, when used for plain text, it replaces the non-visible space with a visible representation:

* ++``Hello, world``{-}++ produces ==Hello, world=={-}.
* ++``Hello, world``++ produces ==Hello, world==.
* ''``Hello, world``{-}'' produces ==Hello, world=={-}.
* ''``Hello, world``'' produces ==Hello, world==.

Attributes may be continued on the next line when a space or line ending character is possible.
In case of a quoted attribute value, the line ending character will be part of the attribute value.
For example:
```
{key="quoted
value"}
```
will produce a value ''quoted\\nvalue'' (where \\n denotes a line ending character).


```
::GREEN::{class=example
background=grey}
```
is allowed, but not
```
::GREEN::{background=color:
green}
```.

However,
```
::GREEN::{background=color:"
green"}
```
is allowed, because line endings are allowed within quotes.

For some block elements, there is a syntax variant if you only want to specify a generic attribute.
For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) block characters.
For [[block-structured elements|00001007030000]], there is a syntax variant if you only want to specify a generic attribute.
For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) characters starting the block.

```
:::attr
...
:::
```
is equivalent to
```
:::{=attr}
...
:::
```.

For other blocks, the closing curly bracket must be on the same line where the block element begins.
However, spaces are allowed between the blocks characters and the attributes.
For block-structured elements, spaces are allowed between the blocks characters and the attributes.
```
=== Heading {style=color:green}
=== Heading {example}
```
is allowed and equivalent to
```
=== Heading{style=color:green}
=== Heading{example}
```.
But
```
=== Heading {style=color:green
background=grey}
```
is not allowed. Same for
```
=== Heading {style=color:"
green"}
```.

For inlines, the attributes must immediately follow the inline markup.
For [[inline-structured elements|00001007040000]], the attributes must immediately follow the inline markup.
However, the attributes may be continued on the next line when a space or line ending character is possible.

``::GREEN::{style=color:green}`` is allowed, but not ``::GREEN:: {style=color:green}``.
``::GREEN::{example}`` is allowed, but not ``::GREEN:: {example}``.

```
::GREEN::{style=color:green
background=grey}
```
is allowed, but not
```
::GREEN::{style=color:
green}
```.

However,
```
::GREEN::{style=color:"
green"}
```
is allowed, because line endings are allowed within quotes.

=== Reference material
* [[Supported attribute values for natural languages|00001007050100]]
* [[Supported attribute values for programming languages|00001007050200]]

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









+


-
+
+
















-
+
-
-
-
-
-
-
-
id: 00001007050100
title: Zettelmarkup: Supported Attribute Values for Natural Languages
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20220827182130

With an [[attribute|00001007050000]] it is possible to specify the natural language of a text region.
This is important, if you want to render your markup into an environment, where this is significant.
HTML is such an environment.

To specify the language within an attribute, you must use the key ''lang''.
The language itself is specified according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].

Examples:
* ``{lang=en}`` for the english language
* ``{lang=en-us}`` for the english dialect spoken in the United States of America
* ``{lang=de}`` for the german language
* ``{lang=de-at}`` for the german language dialect spoken in Austria
* ``{lang=de-de}`` for the german language dialect spoken in Germany

The actual [[typographic quotations marks|00001007040100]] (``""...""``) are derived from the current language.
The language of a zettel (meta key ''lang'') or of the whole Zettelstore (''default-lang'' of the [[configuration zettel|00001004020000#default-lang]]) can be overwritten by an attribute: ``""...""{lang=fr}``{=zmk}.
The language of a zettel (meta key ''lang'') can be overwritten by an attribute: ``""...""{lang=fr}``{=zmk}.
Currently, Zettelstore supports the following primary languages:

* ''de''
* ''en''
* ''fr''

These are used, even if a dialect was specified.

Deleted docs/manual/00001007060000.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: 00001007060000
title: Zettelmarkup: Summary of Formatting Characters
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual

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

|= Character :|= Blocks <|= Inlines <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Typographic quotation mark|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[Tag|00001007040000]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]
| ''&''  | (free) | [[Entity|00001007040000]]
| ''\'''  | (free)  | [[Monospace text|00001007040100]]
| ''(''  | (free) | (free)
| '')''  | (free) | (free)
| ''*''  | [[Unordered list|00001007030200]] | [[strongly emphasized / bold text|00001007040100]]
| ''+''  | (free) | [[Keyboard input|00001007040200]]
| '',''  | (free) | [[Subscripted text|00001007040100]]
| ''-''  | [[Horizonal rule|00001007030400]] | ""[[en-dash|00001007040000]]""
| ''.''  | (free) | [[Horizontal ellipsis|00001007040000]]
| ''/''  | (free) | [[Emphasized / italics text|00001007040100]]
| '':''  | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]]
| '';''  | [[Description term|00001007030100]] | [[Small text|00001007040100]]
| ''<''  | [[Quotation block|00001007030600]] | [[Short inline quote|00001007040100]]
| ''=''  | [[Headings|00001007030300]] | [[Computer output|00001007040200]]
| ''>''  | [[Quotation lists|00001007030200]] | (blocked: to remove anyambiguity with quotation lists)
| ''?''  | (free) | (free)
| ''@''  | (free) | (reserved)
| ''[''  | (reserved)  | [[Link|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]]
| '']''  | (reserved) | End of link, citation key, footnote, mark
| ''^''  | (free) | [[Superscripted text|00001007040100]]
| ''_''  | (free) | [[Underlined text|00001007040100]]
| ''`''  | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]]
| ''{''  | (reserved) | [[Image|00001007040300]], [[Attribute|00001007050000]]
| ''|''  | [[Table row / table cell|00001007031000]] | Separator within link and image formatting
| ''}''  | (reserved) | End of Image, End of Attribute
| ''~''  | (free) | [[Strike-through text|00001007040100]]

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

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

A query expression consists of an optional __[[zettel identifier list|00001007710000]]__, zero or more __[[query directives|00001007720000]]__, an optional __[[search expression|00001007701000]]__, and an optional __[[action list|00001007770000]]__.
The latter two are separated by a vertical bar character (""''|''"", U+007C).

A query expression follows a [[formal syntax|00001007780000]].

* [[List of zettel identifier|00001007710000]]
* [[Query directives|00001007720000]]
** [[Context directive|00001007720300]]
** [[Ident directive|00001007720600]]
** [[Items directive|00001007720900]]
** [[Unlinked directive|00001007721200]]
* [[Search expression|00001007701000]]
** [[Search term|00001007702000]]
** [[Search operator|00001007705000]]
** [[Search value|00001007706000]]
* [[Action list|00001007770000]]

Here are [[some examples|00001007790000]], which can be used to manage a Zettelstore:
{{{00001007790000}}}

Added docs/manual/00001007701000.zettel.



























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007701000
title: Query: Search Expression
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707205043
modified: 20230707210039

In its simplest form, a search expression just contains a string to be search for with the help of a full-text search.
For example, the string ''syntax'' will search for all zettel containing the word ""syntax"".

If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''.
""title"" denotes the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]].
The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match.
""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"".

A search expression may contain more than one search term, such as ''title:syntax''.
Search terms must be separated by one or more space characters, for example ''title:syntax title:search''.
All terms of a select expression must be true so that a zettel is selected.

Above sequence of search expressions may be combined by specifying the keyword ''OR''.
At most one of those sequences must be true so that a zettel is selected.

* [[Search term|00001007702000]]
* [[Search operator|00001007705000]]
* [[Search value|00001007706000]]

Added 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007702000
title: Search term
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20230925173539

A search term allows you to specify one search restriction.
The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions.

A search term can be one of the following (the first three term are collectively called __search literals__):
* A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]].

  All zettel containing the given metadata key with a allowed value (depending on the search operator) are selected.

  If no search value is given, then all zettel containing the given metadata key are selected (or ignored, for a negated search operator).
* An optional [[search operator|00001007705000]], followed by a [[search value|00001007706000]].

  This specifies a full-text search for the given search value.

  However, the operators ""less"" and ""greater"" are not supported, they are internally translated into the ""match"" operators.
  Similar, ""not less"" and ""not greater"" are translated into ""not match"".
  It simply does not make sense to search the content of all zettel for words less than a specific word, for example.

  **Note:** the search value will be normalized according to Unicode NKFD, ignoring everything except letters and numbers.
  Therefore, the following search expression are essentially the same: ''"search syntax"'' and ''search syntax''.
  The first is a search expression with one search value, which is normalized to two strings to be searched for.
  The second is a search expression containing two search values, giving two string to be searched for.
* A metadata key followed by ""''?''"" or ""''!?''"".

  Is true, if zettel metadata contains / does not contain the given key.
* The string ''OR'' signals that following search literals may occur alternatively in the result.

  Since search literals may be negated, it is possible to form any boolean search expression.
  Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]].

  It has no effect on the following search terms initiated with a special uppercase word.
* The string ''PICK'', followed by a non-empty sequence of spaces and a number greater zero (called ""N"").

  This will pick randomly N elements of the result list, preserving the order of that list.
  A zero value of N will produce the same result as if nothing was specified.
  If specified multiple times, the lower value takes precedence.

  Example: ''PICK 5 PICK 3'' will be interpreted as ''PICK 3''.
* The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list.
  If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed.

  Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSE published'' will return a reversed result order.

  An explicit order field will take precedence over the random order described below.

  If no random order is effective, a ``ORDER REVERSE id`` will be added.
  This makes the sort stable.

  Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE id``.

  Any ordering by zettel identifier will make following order terms to be ignored.

  Example: ``ORDER id ORDER created`` will be interpreted as ``ORDER id``.
* The string ''RANDOM'' will provide a random order of the resulting list.

  Currently, only the first term specifying the order of the resulting list will be used.
  Other ordering terms will be ignored.

  A random order specification will be ignored, if there is an explicit ordering given.

  Example: ''RANDOM ORDER published'' will be interpreted as ''ORDER published''.
* The string ''OFFSET'', followed by a non-empty sequence of spaces and a number greater zero (called ""N"").

  This will ignore the first N elements of the result list, based on the specified sort order.
  A zero value of N will produce the same result as if nothing was specified.
  If specified multiple times, the higher value takes precedence.

  Example: ''OFFSET 4 OFFSET 8'' will be interpreted as ''OFFSET 8''.
* The string ''LIMIT'', followed by a non-empty sequence of spaces and a number greater zero (called ""N"").

  This will limit the result list to the first N elements, based on the specified sort order.
  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: ''LIMIT 4 LIMIT 8'' will be interpreted as ''LIMIT 4''.

You may have noted that the specifications of first two items overlap somehow.
This is resolved by the following rule:
* A search term containing no [[search operator character|00001007705000]] is treated as a full-text search.
* The first search operator character found in a search term divides the term into two pieces.
  If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search.
* Otherwise, the search term is treated as a full-text search.

If a term like ''PICK'', ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search.
For example, ''ORDER 123'' will search for a zettel containing the strings ""ORDER"" (case-insensitive) and ""123"".

Added 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
33
34
35
36
37
38
39
40
41
42
43
44
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007705000
title: Search operator
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20230612180539

A search operator specifies how the comparison of a search value and a zettel should be executed.
Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters.

The following are allowed search operator characters:
* The exclamation mark character (""''!''"", U+0021) negates the meaning.
* The equal sign character (""''=''"", U+003D) compares on equal content (""equals operator"").
* The tilde character (""''~''"", U+007E) compares on matching (""match operator"").
* The left square bracket character (""''[''"", U+005B) matches if there is some prefix (""prefix operator"").
* The right square bracket character (""'']''"", U+005D) compares a suffix relationship (""suffix operator"").
* The colon character (""'':''"", U+003A) compares depending on the on the actual [[key type|00001006030000]] (""has operator"").
  In most cases, it acts as a equals operator, but for some type it acts as the match operator.
* The less-than sign character (""''<''"", U+003C) matches if the search value is somehow less then the metadata value (""less operator"").
* The greater-than sign character (""''>''"", U+003E) matches if the search value is somehow greater then the metadata value (""greater operator"").
* The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"").
  In this case no [[search value|00001007706000]] must be given.

Since the exclamation mark character can be combined with the other, there are 18 possible combinations:
# ""''!''"": is an abbreviation of the ""''!~''"" operator.
# ""''~''"": is successful if the search value matched the value to be compared.
# ""''!~''"": is successful if the search value does not match the value to be compared.
# ""''=''"": is successful if the search value is equal to one word of the value to be compared.
# ""''!=''"": is successful if the search value is not equal to any word of the value to be compared.
# ""''[''"": is successful if the search value is a prefix of the value to be compared.
# ""''![''"": is successful if the search value is not a prefix of the value to be compared.
# ""'']''"": is successful if the search value is a suffix of the value to be compared.
# ""''!]''"": is successful if the search value is not a suffix of the value to be compared.
# ""'':''"": is successful if the search value is has/match one word of the value to be compared.
# ""''!:''"": is successful if the search value is not match/has to any word of the value to be compared.
# ""''<''"": is successful if the search value is less than the value to be compared.
# ""''!<''"": is successful if the search value is not less than, e.g. greater or equal than the value to be compared.
# ""''>''"": is successful if the search value is greater than the value to be compared.
# ""''!>''"": is successful if the search value is not greater than, e.g. less or equal than the value to be compared.
# ""''?''"": is successful if the metadata contains the given key.
# ""''!?''"": is successful if the metadata does not contain the given key.
# ""''''"": a missing search operator can only occur for a full-text search.
  It is equal to the ""''~''"" operator.

Added docs/manual/00001007706000.zettel.











1
2
3
4
5
6
7
8
9
10
+
+
+
+
+
+
+
+
+
+
id: 00001007706000
title: Search value
role: manual
tags: #manual #search #zettelstore
syntax: zmk
modified: 20220807162031

A search value specifies a value to be searched for, depending on the [[search operator|00001007705000]].

A search value should be lower case, because all comparisons are done in a case-insensitive way and there are some upper case keywords planned.

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

A context directive calculates the __context__ of a list of zettel identifier.
It starts with the keyword ''CONTEXT''.

Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters.
These are:
* ''FULL'': additionally search for zettel with the same tags,
* ''BACKWARD'': search for context only though backward links,
* ''FORWARD'': search for context only through forward links,
* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17),
* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200).

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

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

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

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

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

Added docs/manual/00001007720600.zettel.













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

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

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

Added docs/manual/00001007720900.zettel.








































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

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

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

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

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


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

Added docs/manual/00001007721200.zettel.













































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007721200
title: Query: Unlinked Directive
role: manual
tags: #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.

Added 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20240219155949

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

Added docs/manual/00001007790000.zettel.




















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

Added 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007800000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
created: 20220805150154
modified: 20231113191330

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

|= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[marked / highlighted text|00001007040100]]
| ''$''  | (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)
| '',''  | (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)
| ''=''  | [[Headings|00001007030300]] | [[Computer output|00001007040200]]
| ''>''  | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]]
| ''?''  | (free) | (free)
| ''@''  | [[Inline-Zettel block|00001007031200]] | [[Inline-zettel snippet|00001007040200#inline-zettel-snippet]]
| ''[''  | (reserved)  | [[Linked material|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]]
| '']''  | (reserved) | End of [[link|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''^''  | (free) | [[Super-scripted text|00001007040100]]
| ''_''  | (free) | [[Emphasized text|00001007040100]]
| ''`''  | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]]
| ''{''  | [[Transclusion|00001007031100]] | [[Embedded material|00001007040300]], [[Attribute|00001007050000]]
| ''|''  | [[Table row / table cell|00001007031000]] | Separator within link and [[embed|00001007040320]] formatting
| ''}''  | End of [[Transclusion|00001007031100]] | End of embedded material, End of Attribute
| ''~''  | [[Evaluation block|00001007031300]] | [[Deleted text|00001007040100]]

Added docs/manual/00001007900000.zettel.











1
2
3
4
5
6
7
8
9
10
+
+
+
+
+
+
+
+
+
+
id: 00001007900000
title: Zettelmarkup: Tutorial
role: manual
tags: #manual #tutorial #zettelmarkup
syntax: zmk
created: 20220810182917
modified: 20221209191820

* [[First steps|00001007903000]]: learn something about paragraphs, emphasized text, and lists.
* [[Second steps|00001007906000]]: know about links, thematic breaks, and headings.

Added docs/manual/00001007903000.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007903000
title: Zettelmarkup: First Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220810182917
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
The most important concept of Zettelmarkup is the __paragraph__.
Ordinary text is interpreted as part of a paragraph.
Paragraphs are typically separated by one or more blank lines.

Therefore, line endings are more or less ignored within one paragraph.
Zettelmarkup will recognize the end of a line, and sore it as a ""soft break".
A soft break is rendered in most cases as a space character.

Within a paragraph you can style your text with [[special markup|00001007040000]].
Some examples:

|= Zettelmarkup | Rendered output | Instruction
| ''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}.
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,
* Numbered lists.

You produce an unnumbered list element by writing an asterisk character followed by a space character at the beginning of a line.
Since a list typically consists of more than one element, the following elements will also start at their own line:

```zmk
* First item
* Second item
* Third item
```

This is rendered as:

:::example
* First item
* Second item
* Third item
:::

Similar, an numbered list element begins a line with the number sign (sic!) followed by a space character:

```zmk
# First item
# Second item
# Third item
```

This is rendered as:

:::example
# First item
# Second item
# Third item
:::

---
After trying out these markup elements, you might want to continue with the [[second steps|00001007906000]].

Added docs/manual/00001007906000.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: 00001007906000
title: Zettelmarkup: Second Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220811115501
modified: 20220926183427

After you have [[learned|00001007903000]] the basic concepts and markup of Zettelmarkup (paragraphs, emphasized text, and lists), this zettel introduces you into the concepts of links, thematic breaks, and headings.

=== Links
A Zettelstore is much more useful, if you connect related zettel.
If you read a zettel later, this allows you to know about the context of a zettel.
[[Zettelmarkup|00001007000000]] allows you to specify such a connection.
A connection can be specified within a paragraph via [[Links|00001007040310]].

* A link always starts with two left square bracket characters and ends with two right square bracket characters: ''[[...]]''.
* Within these character sequences you specify the [[zettel identifier|00001006050000]] of the zettel you want to reference: ''[[00001007903000]]'' will connect to zettel containing the first steps into Zettelmarkup.
* In addition, you should give the link a more readable description.
  This is done by prepending the description before the reference and use the vertical bar character to separate both: ''[[First Steps|00001007903000]]''.

You are not restricted to reference your zettel.
Alternatively, you might specify an URL of an external website: ''[[Zettelstore|https://zettelstore.de]]''.
Of course, if you just want to specify the URL, you are allowed to omit the description: ''[[https://zettelstore.de]]''

|= Zettelmarkup | Rendered output | Remark
| ''[[00001007903000]]'' | [[00001007903000]] | If no description is given, the zettel identifier acts as a description
| ''[[First Steps|00001007903000]]'' | [[First Steps|00001007903000]] | The description should be chosen so that you are not confused later
| ''[[https://zettelstore.de]]'' | [[https://zettelstore.de]] | A link to an external URL is rendered differently
| ''[[Zettelstore|https://zettelstore.de]]'' | [[Zettelstore|https://zettelstore.de]] | You can use any URL your browser is able to support

Again, you probably see a principle.

=== Thematic Breaks
[[And now for something completely different|https://en.wikipedia.org/wiki/And_Now_for_Something_Completely_Different]].

Sometimes, you want to insert a thematic break into your text, because two paragraphs do not separate enough.
In Zettelmarkup is is done by entering three or more hyphen-minus characters at the beginning of a new line.
You must not include blank lines around this line, but it can be more readable if you want to look at the Zettelmarkup text.

```zmk
First paragraph.
---
Second paragraph.
```

```zmk
First paragraph.

---

Second paragraph.
```

Both are rendered as:
:::example
First paragraph.
---
Second paragraph.
:::

Try it!

This might be the time to relax a rule about paragraphs.
You must not specify a blank line to end a paragraph.
Any Zettelmarkup that must start at the beginning of a new line will end a previous paragraph.
Similar, a blank line must not precede a paragraph.

This applies also to lists, as given in the first steps, as well as other [[similar markup|00001007030000]] you will probably later.

=== Headings
Headings explicitly structure a zettel, similar to thematic breaks, but gives the resulting part a name.

To specify a heading in Zettelmarkup, you must enter at least three equal signs, followed by a space, followed by the text of the heading.
Everything must be one the same line.

The number of equal signs determines the importance of the heading: less equal signs means more important.
Therefore, three equal signs treat a heading as most important.
It is a level-1 heading.
Zettelmarkup supports up to five levels.
To specify such a heading, you must enter seven equal signs, plus the space and the text.
If you enter more than seven equal signs, the resulting heading is still of level 5.

See the [[description of headings|00001007030300]] for more details and examples.

Added docs/manual/00001007990000.zettel.


























































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007990000
title: Zettelmarkup: Cheat Sheet
role: manual
tags: #manual #reference #zettelmarkup
syntax: zmk
created: 20221209191905
modified: 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"", ''##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}}
|[[Footnote|00001007040330]]|''text[^footnote]'' &rarr; text[^footnote]
|[[Special characters / entities|00001007040000]]|''&rarr;'' &rarr; &rarr;, ''&#x2115;'' &rarr; &#x2115;, ''&#8987;'' &rarr; &#8987;

=== Structuring
* [[Heading|00001007030300]]: ''=== Heading'', ''==== Sub-Heading''
* [[Horizontal rule / thematic break|00001007030400]]: ''---''
* [[Paragraphs|00001007030000]] are separated by an empty line
---

=== Lists
[[Unnumbered list|00001007030200]]:
```
* First list item
* Second list item
* Third list item
```
[[Numbered list|00001007030200]]:
```
# Item number 1
# Item number 2
# Item number 3
```
[[Description List|00001007030100]]:
```
; Term
: Definition
; Other Term
: Definition for other term
```
=== [[Tables|00001007031000]]
```
|=Header|Because|Equal Sign
|Cell 1.1|Cell 1.2| Cell 1.3
|Cell 2.1|Cell 2.2
```

Changes to docs/manual/00001008000000.zettel.

1
2
3
4
5

6

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


21
22
23

24
25



26
27
28











29
30

31
32
33
34

35
36
37
38
39
40
41
42
43
44








45
46


47
1
2
3
4
5
6

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


20
21
22
23

24
25

26
27
28
29


30
31
32
33
34
35
36
37
38
39
40
41

42
43



44
45
46
47
48






49
50
51
52
53
54
55
56
57
58
59
60
61





+
-
+












-
-
+
+


-
+

-
+
+
+

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

-
+

-
-
-
+




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


+
+

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

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

* CSS
* HTML template data
* Image formats: GIF, PNG, JPEG, SVG
* Markdown
* Plain text, not further interpreted

The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used.
If it is not given, the key ""''default-syntax''"" will be used (specified in the [[configuration zettel|00001004020000#default-syntax]]).
The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which markup language / data format should be used.
If it is not given, it defaults to ''plain''.
The following syntax values are supported:

; [!css]''css''
; [!css|''css'']
: A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML.
; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png''
; [!draw|''draw'']
: A [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters.
; [!gif|''gif'']; [!jpeg|''jpeg'']; [!jpg|''jpg'']; [!png|''png'']; [!webp|''webp'']
: The formats for pixel graphics.
  Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file.
; [!markdown]''markdown'', [!md]''md''
  Typically the data is stored in a separate file and the syntax is given in the meta-file, which has the same name as the zettel identifier and has no file extension.[^Before version 0.2, the meta-file had the file extension ''.meta'']
; [!html|''html'']
: Hypertext Markup Language, will not be parsed further.
  Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]).

  Since HTML from unknown sources may contain security-related problems, zettel with this syntax are treated as an empty zettel, unless the startup configuration value for [[''insecure-html''|00001004010000#insecure-html]] is set to at least the value ""html"".

  For security reasons, equivocal elements will not be encoded in the HTML format / web user interface.
  The ``< script ...>`` tag is an example.
  See [[security aspects of Markdown|00001008010000#security-aspects]] for some details.
; [!markdown|''markdown''], [!md|''md'']
: For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]].
  Since the world of Markdown is so diverse, a [[CommonMark|https://commonmark.org/]] parser is used.
  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.
; [!none]''none''
; [!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''
: A [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]].
  The icon for external material is an example.
; [!text]''text'', [!plain]''plain'', [!txt]''txt''
: Just plain text that must not be interpreted further.
; [!zmk]''zmk''
; [!svg|''svg'']
: [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]].
; [!sxn|''sxn'']
: S-Expressions, as implemented by [[sx|https://zettelstore.de/sx]].
  Often used to specify templates when rendering a zettel as HTML for the [[web user interface|00001014000000]] (with the help of sxhtml]).
; [!text|''text''], [!plain|''plain''], [!txt|''txt'']
: Plain text that must not be interpreted further.
; [!zmk|''zmk'']
: [[Zettelmarkup|00001007000000]].

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

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

Changes to docs/manual/00001008010000.zettel.

1
2
3
4
5

6

7
8
9

10
11
12
13

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

30
31
32
33
34


35
36
37
38
39
40
41


42


43
44
45

46
47

1
2
3
4
5
6

7
8
9

10
11
12
13

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

30
31
32
33


34
35
36
37
38
39
40
41
42
43
44

45
46
47
48

49
50

51





+
-
+


-
+



-
+















-
+



-
-
+
+







+
+
-
+
+


-
+

-
+
id: 00001008010000
title: Use Markdown within Zettelstore
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210518102155
modified: 20221018115601

If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision.
Zettelstore supports [[CommonMark|https://commonmark.org/]], an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown.
Zettelstore supports the [[CommonMark|00001008010500]] dialect of Markdown.

=== Use Markdown as the default markup language of Zettelstore

Add the key ''default-syntax'' with a value of ''md'' or ''markdown'' to the [[configuration zettel|00000000000100]].
Update the [[New Zettel|00000000090001]] template (and other relevant template zettel) by setting the syntax value to ''md'' or ''markdown''.
Whether to use ''md'' or ''markdown'' is not just a matter to taste.
It also depends on the value of [[''zettel-file-syntax''|00001004020000#zettel-file-syntax]] and, to some degree, on the value of [[''yaml-header''|00001004020000#yaml-header]].

If you set ''yaml-header'' to true, then new content is always stored in a file with the extension ''.zettel''.

Otherwise ''zettel-file-syntax'' lists all syntax values, where its content should be stored in a file with the extension ''.zettel''.

If neither ''yaml-header'' nor ''zettel-file-syntax'' is set, new content is stored in a file where its file name extension is the same as the syntax value of that zettel.
In this case it makes a difference, whether you specify ''md'' or ''markdown''.
If you specify the syntax ''md'', your content will be stored in a file with the ''.md'' extension.
Similar for the syntax ''markdown''.

If you want to process the files that store the zettel content, e.g. with some other Markdown tools, this may be important.
Not every Markdown tool allows both file extensions.

BTW, metadata is stored in a file with the extension ''.meta'', if neither ''yaml-header'' nor ''zettel-file-syntax'' is set.
BTW, metadata is stored in a file without a file extension, if neither ''yaml-header'' nor ''zettel-file-syntax'' is set.

=== Security aspects

You should be aware that Markdown is a superset of HTML.
Any HTML code is valid Markdown code.
You should be aware that Markdown is a super-set of HTML.
The body of any HTML document is also a valid Markdown document.
If you write your own zettel, this is probably not a problem.

However, if you receive zettel from others, you should be careful.
An attacker might include malicious HTML code in your zettel.
For example, HTML allows to embed JavaScript, a full-sized programming language that drives many web sites.
When a zettel is displayed, JavaScript code might be executed, sometimes with harmful results.

By default, Zettelstore prohibits any HTML content.
If you want to relax this rule, you should take a look at the startup configuration key [[''insecure-html''|00001004010000#insecure-html]].
Zettelstore mitigates this problem by ignoring suspicious text when it encodes a zettel as HTML.

Even if you have allowed HTML content, Zettelstore mitigates some of the security problems by ignoring suspicious text when it encodes a zettel as HTML.
Any HTML text that might contain the ``<script>`` tag or the ``<iframe>`` tag is ignored.
This may lead to unexpected results if you depend on these.
Other encoding [[formats|00001012920500]] may still contain the full HTML text.
Other [[encodings|00001012920500]] may still contain the full HTML text.

Any external client of Zettelstore, which does not use Zettelstore's HTML encoding, must be programmed to take care of malicious code.
Any external client of Zettelstore, which does not use Zettelstore's [[HTML encoding|00001012920510]], must be programmed to take care of malicious code.

Added docs/manual/00001008010500.zettel.






























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001008010500
title: CommonMark
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk
created: 20220113183435
modified: 20221018123145
url: https://commonmark.org/

[[CommonMark|https://commonmark.org/]] is a Markdown dialect, an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown by providing an unambiguous syntax specification for Markdown, together with a suite of comprehensive tests to validate implementation.

Time will show, if this attempt is successful.

However, CommonMark is a well specified Markdown dialect, in contrast to most (if not all) other dialects.
Other software adopts CommonMark somehow, notably [[GitHub Flavored Markdown|https://github.github.com/gfm/]] (GFM).
But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed.
Plus, they sometimes build on an older specification of CommonMark.

Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]].
If possible, Zettelstore will adapt to newer versions when they are available.

To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark.
Internally, CommonMark is translated into some kind of super-set of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself super-sets of HTML.]
This Zettelmarkup super-set is later [[encoded|00001012920500]], often into [[HTML|00001012920510]].
Because Zettelstore HTML encoding philosophy differs a little bit to that of CommonMark, Zettelstore itself will not pass the CommonMark test suite fully.
However, no CommonMark language element will fail to be encoded as HTML.
In most cases, the differences are not visible for an user, but only by comparing the generated HTML code.

Be aware, depending on the value of the startup configuration key [[''insecure-html''|00001004010000#insecure-html]], HTML code found within a CommonMark document or within the mentioned kind of super-set of Zettelmarkup will typically be ignored for security-related reasons.

Added docs/manual/00001008050000.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001008050000
title: The "draw" language
role: manual
tags: #graphic #manual #zettelstore
syntax: zmk
created: 20220131142036
modified: 20230403123738

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

Technically spoken, the drawing is translated to a [[SVG|00001008000000#svg]] element.

The following characters are interpreted to create a graphical representation.
Some of them will start a path that results in a recognized object.

|=Character:|Meaning|Path Start:
| ''+'' | Corner|Yes
| ''-'' | Horizontal line|Yes
| ''|'' | Vertical line|Yes
| ''<'' | Left arrow|Yes
| ''>'' | Right arrow|No
| ''v'' | Down arrow|No
| ''^'' | Up arrow|Yes
| '':'' | Dashed vertical line|Yes
| ''='' | Dashed horizontal line|Yes
| ''.'' | Rounded corner|Yes
| ''\''' | Rounded corner|Yes
| ''/''  | North-east diagonal line|Yes
| ''\\'' | South-east diagonal line|Yes
| ''x'' | A tick on a line|No
| ''*'' | A dot on a line|No

Interpretation of these characters starts at the top left corner and continues depending on the current character.

All other characters are treated as text.

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

18
19
20
21
22
23
24
25





+
+










-
+







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

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

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

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

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

35
36
37
38
39
40
41
42

43
44

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

63
64
65
66
37
38
39
40
41
42
43

44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68







-
+

-
+

















-
+




Once you have enabled authentication, it is possible to allow others to access your Zettelstore.
Maybe, others should be able to read some or all of your zettel.
Or you want to allow them to create new zettel, or to change them.
It is up to you.

If someone is authenticated as the owner of the Zettelstore (hopefully you), no restrictions apply.
But as an owner, you can create ""user zettel"" to allow others to access your Zettelstore in various ways.
Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the API.
Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the [[API|00001012000000]].

Additionally, you can specify that a zettel is publicily visible.
Additionally, you can specify that a zettel is publicly visible.
In this case no one has to authenticate itself to see the content of the zettel.
Or you can specify that a zettel is visible only to the owner.
In this case, no authenticated user will be able to read and change that protected zettel.

* [[Visibility rules for zettel|00001010070200]]
* [[User roles|00001010070300]] define basic rights of an user
* [[Authorization and read-only mode|00001010070400]]
* [[Access rules|00001010070600]] define the policy which user is allowed to do what operation.

=== Encryption
When Zettelstore is accessed remotely, the messages that are sent between Zettelstore and the client must be encrypted.
Otherwise, an eavesdropper could fetch sensible data, such as passwords or precious content that is not for the public.

The Zettelstore itself does not encrypt messages.
But you can put a server in front of it, which is able to handle encryption.
Most generic web server software do allow this.

To enforce encryption, [[authentication sessions|00001010040700]] are marked as secure by default.
To enforce encryption, [[authenticated sessions|00001010040700]] are marked as secure by default.
If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration.
Otherwise, authentication will not work.

* [[Use a server for encryption|00001010090100]]

Changes to docs/manual/00001010040100.zettel.

1
2
3
4
5

6
7
8
9


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





+




+
+
id: 00001010040100
title: Enable authentication
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
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/00001010040200.zettel.

1
2

3
4
5


6
7
8

9
10
11
12
13
14
15
16
17
18







19
20
21
22
23

24
25
26
27
28
29
30
1
2
3
4
5

6
7
8
9

10
11
12
13
14
15


16
17
18
19
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37


+


-
+
+


-
+





-
-



+
+
+
+
+
+
+




-
+







id: 00001010040200
title: Creating an user zettel
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20221205160251

All data to be used for authenticating a user is store in a special zettel called ""user zettel"". 
A user zettel must have set the following three metadata fields:
A user zettel must have set the following two metadata fields:

; ''user-id'' (""user identification"")
: The unique identification to be specified for authentication.
; ''credential''
: A hashed password as generated by the [[``zettelstore password``{=sh}|00001004051400]] command.
; ''role''
: Must contain the value ""user"".

The title of the zettel typically specifies the real name of the user.

The following metadata elements are optional:

; ''user-role''
: Associate the user with some basic privileges, e.g. a [[user role|00001010070300]]

A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]].

A user zettel can only be created by the owner of the Zettelstore.

The owner should execute the following steps to create a new user zettel:

# Create a new zettel with the role ""user"".
# Create a new zettel.
# Save the zettel to get a [[identifier|00001006050000]] for this zettel.
# Choose a unique identification for the user.
#* If the identifier is not unique, authentication will not work for this user.
# Execute the [[``zettelstore password``|00001004051400]] command.
#* You have to specify the user identification and the zettel identifier
#* If you should not know the password of the new user, send her/him the user identification and the user zettel identifier, so that the person can create the hashed password herself.
# Edit the user zettel and add the hashed password under the meta key ''credential'' and the user identification under the key ''user-id''.

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

17


+


-
+







-



-
+
id: 00001010040400
title: Authentication process
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual
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.
  Or in other words: the oldest zettel is selected[^This is done to prevent an attacker from creating a new note with the same user identification].
# If the zettel does not have role ''user'', authentication fails.
# If the zettel does not have a value for the meta key ''credential'', authentication fails.
# The value of the meta key ''credential'' is compared with the given password.
  If they do not match, authentication fails.
The authentication is successful, because the Zettelstore has an owner, the identifier matches a user zettel, and the password conforms to the stored credential.
The authentication is successful, because the Zettelstore has an owner, the identifier matches an [[user zettel|00001010040200]], and the password conforms to the stored credential.

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
1
2
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: 00001010040700
title: Access token
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
modified: 20211202120950

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 interface, the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]].
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.
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-timeout-api'' value of the startup configuration.
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''.
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/00001010070200.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









1
2
3
4
5
6

7
8
9
10
11
12

13
14

15
16
17
18
19
20

21
22
23
24


25
26
27
28
29
30


31
32
33

34


35
36

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





+
-
+





-
+

-
+



+
+
-
+



-
-
+
+




-
-
+
+

-
+
-
-
+

-
+




+
+
+
+
+
+
+
+
+
id: 00001010070200
title: Visibility rules for zettel
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210510194652
modified: 20220923104643

For every zettel you can specify under which condition the zettel is visible to others.
This is controlled with the metadata key [[''visibility''|00001006020000#visibility]].
The following values are supported:

; [!public]""public""
; [!public|""public""]
: The zettel is visible to everybody, even if the user is not authenticated.
; [!login]""login""
; [!login|""login""]
: Only an authenticated user can access the zettel.

  This is the default value for [[''default-visibility''|00001004020000#default-visibility]].
; [!creator|""creator""]
: Only an authenticated user that is allowed to create new zettel can access the zettel.
; [!owner]""owner""
; [!owner|""owner""]
: Only the owner of the Zettelstore can access the zettel.

  This is for zettel with sensitive content, e.g. the [[configuration zettel|00001004020000]] or the various zettel that contains the templates for rendering zettel in HTML.
; [!expert]""expert""
: Only the owner of the Zettelstore can access the zettel, if runtime configuration [[''expert-mode''|00001004020000#expert-mode]] is set to a boolean true value.
; [!expert|""expert""]
: Only the owner of the Zettelstore can access the zettel, if runtime configuration [[''expert-mode''|00001004020000#expert-mode]] is set to a [[boolean true value|00001006030500]].

  This is for zettel with sensitive content that might irritate the owner.
  Computed zettel with internal runtime information are examples for such a zettel.

When you install a Zettelstore, only some zettel have visibility ""public"".
One is the zettel that contains CSS for displaying the web interface.
When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"".
One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]].
This is to ensure that the web interface looks nice even for not authenticated users.
Another is the zettel containing the [[version|00000000000001]] of the Zettelstore.
Another is the zettel containing the Zettelstore [[license|00000000000004]].
Yet other zettel lists the Zettelstore [[license|00000000000004]], its [[contributors|00000000000005]], and external, licensed [[dependencies|00000000000006]], such as program code written by others or graphics designed by others.
The [[default image|00000000040001]], used if an image reference is invalid,is also public visible.
The [[default image|00000000040001]], used if an image reference is invalid, is also public visible.

Please note: if authentication is not enabled, every user has the same rights as the owner of a Zettelstore.
Please note: if [[authentication is not enabled|00001010040100]], every user has the same rights as the owner of a Zettelstore.
This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]].
In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner"").
The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000096'' is stored with the visibility ""expert"".
If you want to show such a zettel, you must set ''expert-mode'' to true.

=== Examples
By using a [[query expression|00001007700000]], you can easily create a zettel list based on the ''visibility'' metadata key:

| public  | [[query:visibility:public]]
| login   | [[query:visibility:login]]
| creator | [[query:visibility:creator]]
| owner   | [[query:visibility:owner]]
| expert  | [[query:visibility:expert]][^Only if [[''expert-mode''|00001004020000#expert-mode]] is enabled, this list will show some zettel.]

Changes to docs/manual/00001010070300.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
18
19
20
21
22
23
24
25
26
27
28
29





+


-
+


-
+


-
+

+
+
+










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

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

; ""reader""
; [!reader|""reader""]
: The user is allowed to read zettel.
  This is the default value for any user except the owner of the Zettelstore.
; ""writer""
; [!writer|""writer""]
: The user is allowed to create new zettel and to change existing zettel.
; [!creator|""creator""]
: The user is only allowed to create new zettel.
  It is also allowed to change its own user zettel.

There are two other user roles, implicitly defined:

; The anonymous user
: This role is assigned to any user that is not authenticated.
  Can only read zettel with visibility [[public|00001010070200]], but cannot change them.
; The owner
: The user that is configured to be the owner of the Zettelstore.
  Does not need to specify a user role in its user zettel.
  Is not restricted in the use of Zettelstore, except when a zettel is marked as [[read-only|00001006020400]].

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


+


-
+

-
+












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

It is possible to enable both the read-only mode of the Zettelstore //and// authentication/authorization.
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
: Zettelstore runs on your local computer and you only work with it.
; Not read-only, with authorization
: Zettelstore is accessed remotely.
  You need authentication to ensure that only valid users access your Zettelstore.
; With read-only, no authorization
: Zettelstore present publicly its full content to everybody.
; With read-only, with authorization
: Nobody is allowed to change the content of the Zettelstore, but only specific zettel should be presented to the public.

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

31
32

33
34
35
36
37

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


+


-
+









-
+





-
+


+





-
+

-
+




-
+













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

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.
# If the user is authenticated and it is the owner, then the operation is allowed.

In the second step, when authentication is enabled and the requesting user is not the owner, everything depends on the requested operation.
In the second step, when [[authentication is enabled|00001010040100]] and the requesting user is not the owner, everything depends on the requested operation.

* Read a zettel:
** If the visibility is ""public"", the access is granted.
** If the visibility is ""owner"", the access is rejected.
** If the user is not authenticated, access is rejected.
** If the zettel requested is an user zettel, reject the access if the users identification is not the same as of the ''ident'' meta key in the zettel.
** If the zettel requested is an [[user zettel|00001010040200]], reject the access if the users identification is not the same as of the ''user-id'' metadata value in the zettel.

   In other words: only the requesting user is allowed to access its own user zettel.
** If the ''user-role'' of the user is ""creator"", reject the access.
** Otherwise the user is authenticated, no sensitive zettel is requested.
   Allow to read the zettel.
* Create a new zettel
** If the user is not authenticated, reject the access.
** If the ''user-role'' of the user is ""reader"", reject the access.
** If the user tries to create an user zettel, the access is rejected.
** If the user tries to create an [[user zettel|00001010040200]], the access is rejected.

   Only the owner is allowed to create user zettel.
   Only the owner of the Zettelstore is allowed to create user zettel.
** In all other cases allow to create the zettel.
* Change an existing zettel
** If the user is not allowed to read the zettel (see above), reject the access.
** If the user is not authenticated, reject the access.
** If the zettel is the user zettel of the authenticated user, proceed as follows:
** 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
13
14
15
16
17
18


19
20
21
22
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





-
+










-
-
+
+




















-
+







id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
modified: 20210511131719
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.
Technically, this is not trivial.
Any client who wants to communicate with your Zettelstore must trust the public encryption key.
Otherwise the client cannot be sure that it is communication with your Zettelstore.
This problem is solved in part with [[Let's Encrypt|https://letsencrypt.org/]],
<<a free, automated, and open certificate authority (CA), run for the public’s benefit.
It is a service provided by the [[Internet Security Research Group|https://www.abetterinternet.org/]]<<.
""a free, automated, and open certificate authority (CA), run for the public’s benefit.
It is a service provided by the [[Internet Security Research Group|https://www.abetterinternet.org/]]"".

Alternatively, you can buy these keys for public-key encryption at ""certificate authorities"" or its dealers.

=== Server software for encryption
The solution of placing a server for encryption in front of an encryption-unaware server is a relatively old one.
There are many different alternatives to choose.

First, there are web servers.
Business-grade web servers must enable encryption.
Most of them allow to forward a request unencrypted to another web server.
Some examples:

* [[Apache Web Server|https://httpd.apache.org/]]: enable [[mod_proxy|http://httpd.apache.org/docs/current/mod/mod_proxy.html]] and configure a reverse proxy.
* [[nginx|https://nginx.org/]]: set-up a reverse proxy with the [[''proxy_pass''|https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass]] directive.
* [[Caddy|https://caddyserver.com/]]: see below for details.

Other software is also possible.
There exists software dedicated for this task of handling the encryption part.
Some examples:

* [[stunnel|https://www.stunnel.org/]] (<<a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code.<<)
* [[stunnel|https://www.stunnel.org/]] (""a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code."")
* [[Traefik|https://traefik.io/]]: set-up a [[router|https://docs.traefik.io/routing/routers/]].

=== Example configuration for Caddy
For the inexperienced owner of a Zettelstore, [[Caddy|https://caddyserver.com/]] is a good option[^In fact, the [[server-based installation procedure|00001003000000]] of Zettelstore was inspired by Caddy.].
Caddy has the capability to automatically fetch appropriately encryption key from Let's Encrypt, without any further configuration.
The only requirement of doing this is that the server must be publicly accessible.

63
64
65
66
67
68
69
70

63
64
65
66
67
68
69

70







-
+
  }
}
```
This will forwards requests with the prefix ""/manual/"" to the running Zettelstore.
All other requests will be handled by Caddy itself.

In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"".
This is to allow Zettelstore ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response.
This is to allow Zettelstore to ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response.

Changes to docs/manual/00001012000000.zettel.

1
2
3
4
5


6
7
8
9
10
11
12

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




34
35
36
37
38
39
40
41
42
43
44
45
46














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

14
15
16
17







18
19
20
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: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231128183617

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

While JSON is the main encoding format, it is possible to retrieve zettel representations in other formats.
If you want to create a new zettel or to change an existing one, you have to use JSON.
There is an [[overview zettel for encoding formats|00001012920500]] that describes the valid formats.

Various parts of a zettel can be retrieved.
There are the [[possible values to specify zettel parts|00001012920800]].

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

=== Zettel lists
* [[List metadata of all zettel|00001012051200]]
* [[List all zettel, but in different encoding formats|00001012051400]]
* [[List all zettel, but include different parts of a zettel|00001012051600]]
* [[Shape the list of zettel metadata with filter options|00001012051800]]
* [[List all zettel|00001012051200]]
* [[Query the list of all zettel|00001012051400]]
* [[Determine a tag zettel|00001012051600]]
* [[Determine a role zettel|00001012051800]]
* [[Sort the list of zettel metadata|00001012052000]]
* [[List all tags|00001012052200]]
* [[List all roles|00001012052400]]

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

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

Changes to docs/manual/00001012050200.zettel.

1
2
3
4
5


6
7
8
9

10
11
12
13
14

15
16
17
18
19
20

21
22
23
24
25
26

27
28
29

30
31
32
33
34









35
36

37
38
39
40
41
42
43
44
45
46
47
1
2
3
4
5
6
7
8
9
10

11
12
13
14
15

16
17
18
19
20
21

22
23
24
25
26
27

28
29
30

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

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





+
+



-
+




-
+





-
+





-
+


-
+





+
+
+
+
+
+
+
+
+

-
+











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

Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]].
This token has to be used for other API calls.
It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[startup configuration|00001004010000]] (typically 10 minutes).
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
("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''
: Authentication failed.
  Either the user identification is invalid or you provided the wrong password.
; ''403''
: Authentication is not active.

Changes to docs/manual/00001012050400.zettel.

1
2
3
4
5


6
7
8
9
10
11
12
13
14

15
16
17
18









19
20
21

22
23
24
25

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

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

32
33
34
35

36
37
38





+
+








-
+




+
+
+
+
+
+
+
+
+


-
+



-
+


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

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

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

```sh
# curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a
{"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
("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 authorization was not [[enabled|00001010040100]], or the access bearer token was not valid.
  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
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


+


-
+







-
+




-
+

-
+
id: 00001012050600
title: API: Provide an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk
role: manual
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.

For example (raw HTTP):
For example (in plain text HTTP):
```
GET /z HTTP/1.0
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg
```
Note, that there is exactly one space character (''U+0020'') between the string ""Bearer"" and the access token: ``Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.ey...``{-}.
Note, that there is exactly one space character (""'' ''{-}"", U+0020) between the string ""Bearer"" and the access token: ``Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.ey...``{-}.

If you use the [[curl|https://curl.haxx.se/]] tool, you can use the ++-H++ command line parameter to set this header field.
If you use the [[curl|https://curl.haxx.se/]] tool, you can use the ''-H'' command line parameter to set this header field.

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
1

2
3
4
5
6
7
8


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

21
22
23
24
25




26


27





28







29





30





31


32
33



34
35
36
37





38





39
40
41
42
43
44
45




46
47
48
49
50




51
52
53
54
55
56








57
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 metadata of all zettel
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170810

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

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

```sh
# curl http://127.0.0.1:23123/z
...
00001012051200 API: List all zettel
00001012050600 API: Provide an access token
{"list":[{"id":"00001012051200","url":"/z/00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","url":"/z/00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","url":"/z/00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","url":"/z/00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","url":"/z/00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]}
00001012050400 API: Renew an access token
00001012050200 API: Authenticate a client
...
```

The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects.
These zettel JSON objects themself contains the keys ''"id"'' (value is a string containing the zettel identifier), ''"url"'' (value is a string containing the URL of the zettel), and ''"meta"'' (value ss a JSON object).
The vlaue 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.

The list is **not** sorted, even in the these examples where it appears to be sorted.
If you reformat the JSON output from the ''GET /z'' call, you'll see its structure better:

If you want to have it ordered, you must specify it with the help of a [[query expression|00001007700000]] / [[search term|00001007702000]].
```json
{
  "list": [
    {
      "id": "00001012051200",
See [[Query the list of all zettel|00001012051400]] how to do it.
      "url": "/z/00001012051200",
      "meta": {
        "title": "API: List for all zettel some data",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      }

    },
    {
      "id": "00001012050600",
      "url": "/z/00001012050600",
      "meta": {
=== Data output
        "title": "API: Provide an access token",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      }

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

      "id": "00001012050400",
      "url": "/z/00001012050400",
      "meta": {
```sh
# curl 'http://127.0.0.1:23123/z?enc=data'
(meta-list (query "") (human "") (list (zettel (id "00001012921200") (meta (title "API: Encoding of Zettel Access Rights") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (box-number "1") (created "00010101000000") (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") (modified "20220201171959") (published "20220201171959")) (rights 62)) (zettel (id "00001007030100") ...
```
        "title": "API: Renew an access token",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      }

    },
    {
      "id": "00001012050200",
      "url": "/z/00001012050200",
      "meta": {
Pretty-printed, this results in:
```
(meta-list (query "")
           (human "")
           (list (zettel (id "00001012921200")
                         (meta (title "API: Encoding of Zettel Access Rights")
                               (role "manual")
        "title": "API: Authenticate a client",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "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")
      }
    },
    {
      "id": "00001012000000",
                               (created "00010101000000")
                               (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300")
                               (modified "20220201171959")
                               (published "20220201171959"))
                         (rights 62))
                 (zettel (id "00001007030100")
      "url": "/z/00001012000000",
      "meta": {
        "title": "API",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      }
    }
```

* The result is a list, starting with the symbol ''meta-list''.
* Then, some key/value pairs are following, also nested.
* Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]].
* ''list'' starts a list of zettel.
* ''zettel'' itself start, well, a zettel.
* ''id'' denotes the zettel identifier, encoded as a string.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.

  ]
}
```
In this special case, the metadata of each zettel just contains the four default keys ''title'', ''tags'', ''syntax'', and ''role''.

If you specify the request in above simple way, you will always get a JSON object that contains all zettel maintained by your Zettelstore and which contains just the metadata of each zettel.
There are several ways to change this behavior.
=== 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.

* [[Specify a different encoding format|00001012051400]]
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).
* [[Specify which detail information of a zettel should be included|00001012051600]]

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

1

2
3
4
5

6
7
8
9
10



11
12
13
14
15
16
17
18
19
20




21
22
23
24
25
26
27
28
29
30
31
32
33
34
35


36
37
38







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



58
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

-
+
+


-
+
+
+

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

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


-
-
+
+

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

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

-
+
+



-
+
+
+
+
+
+

-
+

-
-
-
+
id: 00001012051400
title: API: List all zettel, but in different encoding formats
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
role: manual
created: 20220912111111
modified: 20240219161831
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.
You can add a query parameter ''_format=[[FORMAT|00001012920500]]'' to select the encoding format when [[retrieving all zettel|00001012051200]].
Probably some formats are not very useful and may not make sense.
Currently implemented are the formats [[''json''|00001012920501]] (default), [[''djson''|00001012920503]], and [[''html''|00001012920510]].

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.
The format ''json'' will be selected, if no ''_format'' is specified.
''djson'' will return a JSON object with some more detailed information.
For example, the [[Zettelmarkup|00001007000000]] structure will be returned and tags are returned as a list of strings.
''html'' will return an HTML encoding of the zettel list, using a HTML unnumbered list.

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

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

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

```sh
# curl 'http://127.0.0.1:23123/z?_format=djson'
{"list":[{"id":"00001012051400","url":"/z/00001012051400?_format=djson","meta":{"title":[{"t":"Text","s":"API:"},{"t":"Space"},{"t":"Text","s":"List"},{"t":"Space"},{"t":"Text","s":"all"},{"t":"Space"},{"t":"Text","s":"zettel,"},{"t":"Space"},{"t":"Text","s":"but"},{"t":"Space"},{"t":"Text","s":"in"},{"t":"Space"},{"t":"Text","s":"different"},{"t":"Space"},{"t":"Text","s":"encoding"},{"t":"Space"},{"t":"Text","s":"formats"}],"tags":["#api","#manual","#zettelstore"],"syntax":"zmk","role":"manual"}}, ...
# 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") ...
```
Or reformatted:
````json
{
  "list": [
    {
      "id": "00001012051400",
      "url": "/z/00001012051400?_format=djson",

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

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

=== Aggregates

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

            "t": "Text",
            "s": "API:"
          },
          {

            "t": "Space"
          },
          {
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''.

            "t": "Text",
            "s": "List"
```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
          },
          {
            "t": "Space"
          },
          {
            "t": "Text",
            "s": "all"
          },
          {
```

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

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

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

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

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

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

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

```sh
# curl 'http://127.0.0.1:23123/z?_format=html'
<html lang="en">
<body>
<ul>
<li><a href="/z/00001012051400?_format=html">API: List all zettel, but in different encoding formats</a></li>
<li><a href="/z/00001012051200?_format=html">API: List metadata of all zettel</a></li>
<li><a href="/z/00001012050600?_format=html">API: Provide an access token</a></li>
<li><a href="/z/00001012050400?_format=html">API: Renew an access token</a></li>
<li><a href="/z/00001012050200?_format=html">API: Authenticate a client</a></li>
<li><a href="/z/00001012000000?_format=html">API</a></li>
</ul>
</body>
</html>```
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.
; ''REDIRECT'' (aggregate)
: Performs a HTTP redirect to the first selected zettel, using HTTP status code 302.
  The zettel identifier is in the body.
; ''REINDEX'' (aggregate)
: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call.
  It is not really an aggregate, since it is used only for its side effect.
  It is allowed to specify another aggregate.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case.

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

If you request a JSON-based encoding format, you can request additional [[parts of a zettel|00001012051600]].
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''
: Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]].
: 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.
: Request was not valid. 
  There are several reasons for this.
  Maybe you provided a wrong value for ''_format''.
; ''501''
: Encoding format is not yet implemented, but will be in the future.
  Maybe the access bearer token was not valid, or you forgot to specify a valid query.

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

2
3
4
5

6
7
8

9
10



11
12
13
14

15
16
17
18

19




20
21





22



23




24
25



26
27


28
29
30








31
32
33



34
35
36




37
38
39
40
41
42
43





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


57
58
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: List all zettel, but include different parts of a zettel
title: API: Determine a tag zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
role: manual
created: 20230928183339
modified: 20230929114937

For JSON-based formats[^[[''json''|00001012920501]] and [[''djson''|00001012920503]]] you can add a query parameter ''_part=[[PART|00001012920800]]'' to select which parts of a zettel must be encoded.
The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""tag zettel"", i.e. a zettel that documents a given tag.

All allowed parts are implemented.
The JSON keys ''"id"'' and ''"url"'' are always included.
The default value is ''_part=meta''.
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 you just want to know the zettel identifier and the zettel url, specify ''_part=id'':
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 'http://127.0.0.1:23123/z?_format=djson&_part=id'
# curl -i 'http://127.0.0.1:23123/z?tag=%23api'
{"list":[{"id":"00001012051600","url":"/z/00001012051600?_format=djson"},{"id":"00001012051400","url":"/z/00001012051400?_format=djson"},{"id":"00001012051200","url":"/z/00001012051200?_format=djson"},{"id":"00001012050600","url":"/z/00001012050600?_format=djson"},{"id":"00001012050400","url":"/z/00001012050400?_format=djson"},{"id":"00001012050200","url":"/z/00001012050200?_format=djson"},{"id":"00001012000000","url":"/z/00001012000000?_format=djson"}]}
```
Or reformatted:
````json
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
{
  "list": [
    {
      "id": "00001012051600",
      "url": "/z/00001012051600?_format=djson"
Location: /z/00001019990010
    },
    {
      "id": "00001012051400",
Content-Length: 14
      "url": "/z/00001012051400?_format=djson"
    },
    {
      "id": "00001012051200",

00001019990010
      "url": "/z/00001012051200?_format=djson"
    },
    {
```

      "id": "00001012050600",
      "url": "/z/00001012050600?_format=djson"
Alternatively, you can omit the ''#'' character at the beginning of the tag:
```sh
# curl -i 'http://127.0.0.1:23123/z?tag=api'
    },
    {
      "id": "00001012050400",
      "url": "/z/00001012050400?_format=djson"
    },
    {
      "id": "00001012050200",
      "url": "/z/00001012050200?_format=djson"
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010
    },
    {
      "id": "00001012000000",
Content-Length: 14

00001019990010
      "url": "/z/00001012000000?_format=djson"
    }
  ]
}
```

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

````

A value ''_part=content'' will include the zettel content.
If ''_format=json'', the content is a string value.
For ''_format=djson'', a detailed JSON value will be returned.
```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 'http://127.0.0.1:23123/z?_part=content'
{"list":[{"id":"00001012051600","url":"/z/00001012051600","content":"For JSON-based formats[^[[''json''|00001012920501]] and [[''djson''|00001012920503]]] you can add a query parameter ''_part=[[PART|00001012920800]]'' to select the encoding format when [[retrieving all zettel|00001012051200]].\n\nAll given parts are implemented.\nThe JSON keys ''\"id\"'' ...
# 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.
The value ''_part=zettel'' will include both the metadata of a zettel //and// its zettel content.
Metadata will additionally include all autogenerated default values, such as the key ''"copyright"'' and ''"license"''.

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
; ''200''
: Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]].
; ''400''
; ''302''
: Tag zettel was found.
  The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier.
; ''404''
: Request was not valid.
  There are several reasons for this.
: No zettel for the given tag was found.
  Maybe you provided a wrong value for ''_part''.

Changes to 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
72
73
74

75

76
77
78
79
80





81
82
83
84

85
86
87



88
89
90

91
92
93

94

1

2
3
4
5
6

7
8


9

10


11



12
13
14

15




16


17
18


19
20
21
22
23
24

25
26



27
28
29
30


31
32
33


34
35





36
37
38
39
40





41
42


43
44
45


46
47




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: Shape the list of zettel metadata with filter options
title: API: Determine a role zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20231128183917
modified: 20210510150129
modified: 20231128184701

In most cases, it is not essential to list //all// zettel.
Typically, you are interested only in a subset of the zettel maintained by your Zettelstore.
The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""role zettel"", i.e. a zettel that documents a given role.
This is done by adding some query parameters to the general ''GET /z'' request.

=== Filter
Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key.
The query parameter ""''role''"" allows you to specify a value that is interpreted as the name of a role.
According to the [[type|00001006030000]] of a metadata key, zettel are matched and therefore filtered.
All [[supported|00001006020000]] metadata keys have a well-defined type.
User-defined keys have the type ''e'' (string, possibly empty).
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.
For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used.
```sh
# curl 'http://127.0.0.1:23123/z?title=API'
{"list":[{"id":"00001012921000","url":"/z/00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","url":"/z/00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","url":"/z/00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ...
```

However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021'').
For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be:
For example, if you want to determine the role zettel for the role ""manual"", your request will be:
```sh
# curl 'http://127.0.0.1:23123/z?title=!API'
{"list":[{"id":"00010000000000","url":"/z/00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","url":"/z/00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}},
# 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
```
The empty query parameter values matches all zettel that contain the given metadata key.
Similar, if you specify just the exclamation mark character as a query parameter value, only those zettel match that does //not// contain the given metadata key.
For example ``curl 'http://localhost:23123/z?back=!&backward='`` returns all zettel that are reachable via other zettel, but also references these zettel.

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.

=== Output only specific parts of a zettel
If you are just interested in the zettel identifier, you should add the ""''_part''"" query parameter:
If you specified some more query parameter, these will be part of the URL in the response header ''Location'':

```sh
# curl 'http://127.0.0.1:23123/z?title=API&_part=id'
{"list":[{"id":"00001012921000","url":"/z/00001012921000"},{"id":"00001012920500","url":"/z/00001012920500"},{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001012051800","url":"/z/00001012051800"},{"id":"00001012051600","url":"/z/00001012051600"},{"id":"00001012051400","url":"/z/00001012051400"},{"id":"00001012051200","url":"/z/00001012051200"},{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012000000","url":"/z/00001012000000"}]}
# curl -i 'http://127.0.0.1:23123/z?role=manual&part=zettel'
HTTP/1.1 302 Found
```
If you want only those zettel that additionally must contain the string ""JSON"", you have to add an additional query parameter:
```sh
# curl 'http://127.0.0.1:23123/z?title=API&_part=id&title=JSON'
{"list":[{"id":"00001012921000","url":"/z/00001012921000"}]}
Content-Type: text/plain; charset=utf-8
Location: /z/20231128184200?part=zettel
Content-Length: 14

20231128184200
```
Similarly, if you add another query parameter, the intersection of both results is returned:
```sh
# curl 'http://127.0.0.1:23123/z?title=API&_part=id&id=00001012050'
{"list":[{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"}]}
```

=== Limit and offset
By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements:
Otherwise, if no role zettel was found, the response will use the HTTP status code 404 (""Not found"").

```sh
# curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2'
{"list":[{"id":"00001012000000","url":"/z/00001012000000"},{"id":"00001012050200","url":"/z/00001012050200"}]}
# curl -i 'http://127.0.0.1:23123/z?role=norole'
HTTP/1.1 404 Not Found
```
The query parameter ""''_offset''"" allows to list not only the first elements, but to begin at a specific element:
```sh
# curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2&_offset=1'
Content-Type: text/plain; charset=utf-8
Content-Length: 30

Role zettel not found: norole
{"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]}
```

=== General filter
The query parameter ""''_s''"" allows to provide a string for a full-text search of all zettel.
The search string will be normalized according to Unicode NKFD, ignoring everything except letters and numbers.

To fulfill this service, Zettelstore will evaluate internally the query ''role:role title=ROLE'', there ''ROLE'' is the actual role.
If the search string starts with the character ""''!''"", it will be removed and the query matches all zettel that **does not match** the search string.

In the next step, the first character of the search string will be inspected.
If it contains one of the characters ""'':''"", ""''=''"", ""''>''"", or ""''~''"", this will modify how the search will be performed.
The character will be removed from the start of the search string.

For example, assume the search string is ""def"":
Of course, if you are only interested in the URL of the role zettel, you can make use of the HTTP ''HEAD'' method:

```sh
; ""'':''"", ""''~''"" (or none of these characters)[^""'':''"" is always the character for specifying the default comparison. In this case, it is equal to ""''~''"". If you omit a comparison character, the default comparison is used.]
: The zettel must contain a word that contains the search string.
  ""def"", ""defghi"", and ""abcdefghi"" are matching the search string.
; ""''=''""
: The zettel must contain a word that is equal to the search string.
# 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
  Only the word ""def"" matches the search string.
; ""''>''""
: The zettel must contain a word with the search string as a prefix.
  A word like ""def"" or ""defghi"" matches the search string.
```

If you want to include an initial ""''!''"" into the search string, you must prefix that with the escape character ""''\\''"".
For example ""\\!abc"" will search for zettel that contains the string ""!abc"".
=== HTTP Status codes
; ''302''
: Role zettel was found.
A similar rule applies to the characters that specify the way how the search will be done.
For example, ""!\\=abc"" will search for zettel that do not contains the string ""=abc"".

  The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier.
You are allowed to specify this query parameter more than once.
All results will be intersected, i.e. a zettel will be included into the list if all of the provided values match.

; ''404''
This parameter loosely resembles the search box of the web user interface.
: No zettel for the given role was found.

Deleted docs/manual/00001012052000.zettel.

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






















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012052000
title: API: Sort the list of zettel metadata
role: manual
tags: #api #manual #zettelstore
syntax: zmk

If not specified, the list of zettel is sorted descending by the value of the zettel identifier.
The highest zettel identifier, which is a number, comes first.
You change that with the ""''_sort''"" query parameter.
Alternatively, you can also use the ""''_order''"" query parameter.
It is an alias.

Its value is the name of a metadata key, optionally prefixed with a hyphen-minus character (""-"", ''U+002D'').
According to the [[type|00001006030000]] of a metadata key, the list of zettel is sorted.
If hyphen-minus is given, the order is descending, else ascending.

If you want a random list of zettel, specify the value ""_random"" in place of the metadata key.
""''_sort=_random''"" (or ""''_order=_random''"") is the query parameter in this case.
If can be combined with ""[[''_limit=1''|00001012051800]]"" to obtain a random zettel.

Currently, only the first occurrence of ''_sort'' is recognized.
In the future it will be possible to specify a combined sort key.

Deleted docs/manual/00001012052200.zettel.

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


















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012052200
title: API: List all tags
role: manual
tags: #api #manual #zettelstore
syntax: zmk

To list all [[tags|00001006020000#tags]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/t''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/t
{"tags":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}
```

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

Please note that this structure will likely change in the future to be more compliant with other API calls.

Deleted docs/manual/00001012052400.zettel.

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


















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012052400
title: API: List all roles
role: manual
tags: #api #manual #zettelstore
syntax: zmk

To list all [[roles|00001006020100]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/r''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/r
{"role-list":["configuration","manual","user","zettel"]}
```

The JSON object only contains the key ''"role-list"'' with the value of a sorted string list.
Each string names one role.

Please note that this structure will likely change in the future to be more compliant with other API calls.

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20230807170416

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


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

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

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

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

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.

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

Added docs/manual/00001012053300.zettel.




























































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20230807170259

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

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

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

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

```sh
...
````

Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''part=PART''.
Valid values for [[''PART''|00001012920800]] are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value).


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

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

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

=== Data output

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

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

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

* The result is a list, starting with the symbol ''zettel''.
* Then, some key/value pairs are following, also nested.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.
* ''"encoding"'' states how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
* The zettel contents is stored as a value of the key ''content''.
  Typically, text content is not encoded, and binary content is encoded via Base64.

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

Changes to docs/manual/00001012053400.zettel.

1
2














3
4
5

6

7


8
9
10
11
12
13


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


39
40
41
42




43








44
45





46
47
48
49

50
51
52
53

54
55
56
57
58
1

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

22
23
24


25


26
27
28




















29



30
31




32
33
34
35

36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52

53
54
55
56

57
58
59
60
61
62

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



+

+
-
+
+

-
-

-
-
+
+

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

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

-
+
+
+
+
+



-
+



-
+





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

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

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

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

=== Data output
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 (14 digits).

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

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/z/00001012053400
{"id":"00001012053400","url":"/z/00001012053400","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ...
# 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))
```
Similar to listing all or some zettel, you can provide a query parameter ''_format=[[FORMAT|00001012920500]]'' to select a different encoding format.
The default encoding format is ""[[json|00001012920501]]"".
Others are ""[[djson|00001012920503]]"", ""[[html|00001012920510]]"", and some more.
```sh
# curl 'http://127.0.0.1:23123/z/00001012053400?_format=html'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>API: Retrieve data for an exisiting zettel</title>
<meta name="keywords" content="api, manual, zettelstore">
<meta name="zs-syntax" content="zmk">
<meta name="zs-role" content="manual">
<meta name="copyright" content="(c) 2020 by Detlef Stern <ds@zettelstore.de>">
<meta name="license" content="CC BY-SA 4.0">
</head>
<body>
<p>The endpoint to work with a specific zettel is <span style="font-family:monospace">/z/{ID}</span>, where <span style="font-family:monospace">{ID}</span> is a placeholder for the zettel identifier (14 digits).</p>
...
```

You also can use the query parameter ''_part=[[PART|00001012920800]]'' to specify which parts of a zettel must be encoded.
In this case, its default value is ''zettel''.
```sh
Pretty-printed, this results in:
```
# curl 'http://192.168.17.7:23121/z/00001012053400?_format=html&_part=meta'
<meta name="zs-title" content="API: Retrieve data for an exisiting zettel">
<meta name="keywords" content="api, manual, zettelstore">
<meta name="zs-syntax" content="zmk">
(list (meta (title "API: Retrieve metadata of an existing zettel")
            (role "manual")
            (tags "#api #manual #zettelstore")
            (syntax "zmk")
<meta name="zs-role" content="manual">
            (back "00001012000000 00001012053300")
            (backward "00001012000000 00001012053300")
            (box-number "1")
            (created "20210726174524")
            (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200")
            (modified "20230703174515")
            (published "20230703174515"))
      (rights 62))
```
(Note that metadata of the whole zettel contains some computed values, such as ''"copyright"'' and ''"license"'', while the metadata for ''_part=meta'' just contain the explicitly specified metadata.)

* 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 or ''_format'' / ''_part'' contained illegal values.
  Maybe the zettel identifier did not consist of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230807170112

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

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

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

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

Changes to docs/manual/00001012053600.zettel.

1
2


3
4
5
6

7
8
9
10

11
12
13
14
15
16
17

18
19
20
21
22
23
24

25
26
27

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51


52
53
54
55
56
57
58
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
1

2
3
4
5


6




7







8







9



10
























11
12











13


14








15

















16



17
18
19
20


21
22

23

24
25

26
27
28
29

30
31
32
33
34
35

-
+
+


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

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

-
-
+
+
-

-


-
+



-
+





id: 00001012053600
title: API: Retrieve references of an existing zettel
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
role: manual

created: 20210126175322
The web of zettel is one important value of a Zettelstore.
Many zettel references other zettel, images, external/local material or, via citations, external literature.
By using the [[endpoint|00001012920000]] ''/l/{ID}'' you are able to retrieve these references.

modified: 20230807170019
````
# curl http://127.0.0.1:23123/l/00001012053600
{"id":"00001012053600","url":"/z/00001012053600","links":{"incoming":[],"outgoing":[{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001007040300","url":"/z/00001007040300#links"},{"id":"00001007040300","url":"/z/00001007040300#images"},{"id":"00001007040300","url":"/z/00001007040300#citation-key"}],"local":[],"external":[]},"images":{"outgoing":[],"local":[],"external":[]},"cites":[]}
````
Formatted, this translates into:
````json
{

  "id": "00001012053600",
  "url": "/z/00001012053600",
  "links": {
    "incoming": [],
    "outgoing": [
      {
        "id": "00001012920000",
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]].
        "url": "/z/00001012920000"
      },
      {

        "id": "00001007040300",
        "url": "/z/00001007040300#links"
      },
      {
        "id": "00001007040300",
        "url": "/z/00001007040300#images"
      },
      {
        "id": "00001007040300",
        "url": "/z/00001007040300#citation-key"
      }
    ],
    "local": [],
    "external": []
  },
  "images": {
    "outgoing": [],
    "local": [],
    "external": []
  },
  "cites": []
}
````
=== Kind
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.
The following to-level JSON keys are returned:
; ''id''
: The zettel identifier for which the references were requested.
; ''url''
: The API endpoint to fetch more information about the zettel.
; ''link''
: A JSON object that contains information about incoming and outgoing [[links|00001007040300#links]].
; ''image''
: A JSON object that contains information about referenced [[images|00001007040300#images]].
; ''cite''
: A JSON list of [[citation keys|00001007040300#citation-key]] (as JSON strings).

The query parameter ''kind'' controls which of these values is retrieved:

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).
|= ''kind'' <| Number >| Returned values
| (nothing) | (14) |links, images, and citation keys
| ''link'' | 2|links 
| ''image'' | 4|images
| ''cite'' | 8|citation keys
| ''both'' | (6)|links and images
| ''all'' | (14)|links, images, and citation keys

For example:
The ""Number"" column gives an indication about an alternative way of specifying the kind.
Every kind is given a number, which you also can use to specify the requested kind of reference.
To get more than one kind, just add the numbers.
If you want to retrieve only images and citations, which was not given a ''kind'' value, just specify ''kind=12''.
=== Matter
If you request to retrieve referenced links and/or referenced images, you can further control, which matter of references should be retrieved.
This is controlled by the value of the query parameter ''matter'':
|= ''matter''| Number >| Returned reference list
| (nothing) | (30) | incoming, outgoing, local, and external references
| ''incoming'' | 2|incoming reference, not allowed for images (aka ""backlinks"", not yet implemented)
| ''outgoing'' | 4|outgoing references
| ''local'' | 8|local references, i.e. local, non-zettel material
| ''external'' |16| external references, i.e. on the web
| ''zettel'' | (6)|incoming and outgoing references
| ''material'' |(24)| local and external references
| ''all'' | (30)| incoming, outgoing, local, and external references

```sh
Incoming and outgoing references are basically zettel.
Therefore the list elements are JSON objects with keys ''id'' and ''url''.
Local and external references are strings.
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ...
```

Similar to the ''kind'' query parameter, each matter is associated with a number.
To retrieve a combination of matter values that does not have a name, just add the numbers.
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.
For example, if you want to retrieve only the outgoing and the external references, specify ''matter=20''.

If a list is not going to retrieved, the associated value is ``null``{=json} instead of an empty list.
=== 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 ''_format'' / ''_part'' contained illegal values.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Deleted docs/manual/00001012053800.zettel.

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
















































































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

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]].
Zettel are also connected by using same [[tags|00001006020000#tags]].

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

Zettel with same tags as the origin zettel are considered depth 1.
Only for the origin zettel, tags are used to calculate a connection.
Currently, only some of the newest zettel with a given tag are considered a connection.[^The number of zettel is given by the value of parameter ''depth''.]
Otherwise the context would become too big and therefore unusable.

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

````
# curl 'http://127.0.0.1:23123/y/00001012053800?limit=3&dir=forward&depth=2'
{"id": "00001012053800","url": "/z/00001012053800","meta": {...},"list": [{"id": "00001012921000","url": "/z/00001012921000","meta": {...}},{"id": "00001012920800","url": "/z/00001012920800","meta": {...}},{"id": "00010000000000","url": "/z/00010000000000","meta": {...}}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001012053800",
  "url": "/z/00001012053800",
  "meta": {...},
  "list": [
    {
      "id": "00001012921000",
      "url": "/z/00001012921000",
      "meta": {...}
    },
    {
      "id": "00001012920800",
      "url": "/z/00001012920800",
      "meta": {...}
    },
    {
      "id": "00010000000000",
      "url": "/z/00010000000000",
      "meta": {...}
    }
  ]
}
````
=== Keys
The following top-level JSON keys are returned:
; ''id''
: The zettel identifier for which the context was requested.
; ''url''
: The API endpoint to fetch more information about the zettel.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''list''
: A list of JSON objects with keys ''id'', ''url'' and ''meta'' 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/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


















































































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

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","url":"/z/00001000000000","meta":{...},"list":[{"id":"00001001000000","url":"/z/00001001000000","meta":{...}},{"id":"00001002000000","url":"/z/00001002000000","meta":{...}},{"id":"00001003000000","url":"/z/00001003000000","meta":{...}},{"id":"00001004000000","url":"/z/00001004000000","meta":{...}},...,{"id":"00001014000000","url":"/z/00001014000000","meta":{...}}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001000000000",
  "url": "/z/00001000000000",
  "order": [
    {
      "id": "00001001000000",
      "url": "/z/00001001000000",
      "meta": {...}
    },
    {
      "id": "00001002000000",
      "url": "/z/00001002000000",
      "meta": {...}
    },
    {
      "id": "00001003000000",
      "url": "/z/00001003000000",
      "meta": {...}
    },
    {
      "id": "00001004000000",
      "url": "/z/00001004000000",
      "meta": {...}
    },
    ...
    {
      "id": "00001014000000",
      "url": "/z/00001014000000",
      "meta": {...}
    }
  ]
}
````
=== Kind
The following top-level JSON keys are returned:
; ''id''
: The zettel identifier for which the references were requested.
; ''url''
: The API endpoint to fetch more information about the zettel.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''list''
: A list of JSON objects with keys ''id'', ''url'', and ''meta'' 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.

Added docs/manual/00001012054200.zettel.









































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012054200
title: API: Update a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
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 PUT --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200
```

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

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.

=== HTTP Status codes
; ''204''
: Update was successful, there is no body in the response.
; ''400''
: Request was not valid.
  For example, the request body was not valid.
; ''403''
: You are not allowed to delete the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

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

Added docs/manual/00001012054600.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: 00001012054600
title: API: Delete a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20221219154608

Deleting a zettel within the Zettelstore is executed on the first [[box|00001004011200]] that contains that zettel.
Zettel with the same identifier, but in subsequent boxes remain.
If the first box containing the zettel is read-only, deleting that zettel will fail, as well for a Zettelstore in [[read-only mode|00001004010000#read-only-mode]] or if [[authentication is enabled|00001010040100]] and the user has no [[access right|00001010070600]] to do so.

The [[endpoint|00001012920000]] to delete a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
You must send a HTTP DELETE request to this endpoint:
```
# curl -X DELETE http://127.0.0.1:23123/z/00001000000000
```

=== HTTP Status codes
; ''204''
: Delete was successful, there is no body in the response.
; ''403''
: You are not allowed to delete the given zettel.
  Maybe you do not have enough access rights, or either the box or Zettelstore itself operate in read-only mode.
; ''404''
: Zettel not found.
  You probably specified a zettel identifier that is not used in the Zettelstore.

Added 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
28
29
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012070500
title: API: Retrieve administrative data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220304164242
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'
(0 13 0 "dev" "f781dc384b-dirty")
````

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

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

If any of the three digits has the value -1, its semantic value is unknown.
Similar, the two string might be empty.

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

Added docs/manual/00001012080100.zettel.


















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012080100
title: API: Execute commands
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211230230441
modified: 20220908163125

The [[endpoint|00001012920000]] ''/x'' allows you to execute some (administrative) commands.
To differentiate between the possible commands, you have to set the query parameter ''cmd'' to a specific value:

; ''authenticated''
: [[Check for authentication|00001012080200]]
; ''refresh''
: [[Refresh internal data|00001012080500]]

Other commands will be defined in the future.

Added docs/manual/00001012080200.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: 00001012080200
title: API: Check for authentication
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220103224858
modified: 20220908163156

API clients typically wants 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
# curl -X POST 'http://127.0.0.1:23123/x?cmd=authenticated'
```

If authentication is not enabled, you will get a HTTP status code 200 (OK) with an empty HTTP body.

Otherwise, authentication is enabled.
If you provide a valid access token, you will receive a HTTP status code 204 (No Content) with an empty HTTP body.
If you did not provide a valid access token (with is the typical case), you will get a HTTP status code 401 (Unauthorized), again with an empty HTTP body.

=== HTTP Status codes
; ''200''
: Authentication is disabled.
; ''204''
: Authentication is enabled and a valid access token was provided.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, no query parameter ''cmd'' was given, or it did not contain the value ""authenticate"".
; ''401''
: Authentication is enabled and not valid access token was provided.

Added docs/manual/00001012080500.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: 00001012080500
title: API: Refresh internal data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211230230441
modified: 20220923104836

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.

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.

To avoid stopping and re-starting Zettelstore, you can use the API to force Zettelstore to refresh its internal data if you think it is needed.
To do this, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=refresh''.

```sh
# curl -X POST 'http://127.0.0.1:23123/x?cmd=refresh'
```

If successful, you will get a HTTP status code 204 (No Content) with an empty HTTP body.

The request will be successful if either:
* [[Authentication is enabled|00001010040100]] and you [[provide a valid access token|00001012050600]],
* Authentication is not enabled and you started Zettelstore with the [[run-simple|00001004051100]] command or [[expert-mode|00001004020000#expert-mode]] is set to ""true"".

=== HTTP Status codes
; ''204''
: Operation was successful, the body is empty.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, no query parameter ''cmd'' was given, or it did not contain the value ""refresh"".
; ''403''
: You are not allowed to perform this operation.

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


1
2
3
4
5
6

7
8
9
10

11
12

13
14
15
16
17
18


19
20
21




22
23




24
25
26
27
28

29
30


31
32





+
-
+



-
+

-
+





-
-
+
+

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

-
+

-
-
+
+
id: 00001012920000
title: Endpoints used by the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210511131339
modified: 20230731162343

All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where:
; ''PREFIX''
: is the URL prefix (default: ''/''), configured via the ''url-prefix'' [[startup configuration|00001004010000]],
: is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]],
; ''LETTER''
: is a single letter that specifies the ressource type,
: 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]]
| ''a'' | POST: [[Client authentication|00001012050200]] |
|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic
| ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate
|       | PUT: [[renew access token|00001012050400]] |
| ''l'' |  | GET: [[list references|00001012053600]]
| ''o'' |  | GET: [[list zettel order|00001012054000]]
| ''r'' | GET: [[list roles|00001012052400]]
| ''t'' | GET: [[list tags|00001012052200]]
| ''x'' | GET: [[retrieve administrative data|00001012070500]] | | E**x**ecute
|       | POST: [[execute command|00001012080100]]
| ''y'' |  | GET: [[list zettel context|00001012053800]]
| ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053400]]
|       | POST: add new zettel | PUT: change a zettel
|       |  | DELETE: delete the zettel
| ''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 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''.
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
17
18
19
1

2
3
4
5

6
7
8
9
10
11


12


13
14
15
16
17





-
+
+


-
+
+


+

-
-

-
-
+
+
+


-
-
-
-
id: 00001012920500
title: Formats available by the API
title: Encodings available via the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20230403123653

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

* [[json|00001012920501]] (default)
* [[djson|00001012920503]]
* [[html|00001012920510]]
* [[native|00001012920513]]
* [[raw|00001012920516]]
* [[md|00001012920513]]
* [[shtml|00001012920525]]
* [[sz|00001012920516]]
* [[text|00001012920519]]
* [[zmk|00001012920522]]

Planned formats:
; ''pjson''
: Encoder that emits JSON data that can be fed into [[pandoc|https://pandoc.org]] for further processing.

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
















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920501
title: JSON Format
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

This is the default representation of a zettel or a list of zettel.
Basically, user provided data is encoded as a string (zettel content and metadata values),
The metadata and some other structuring data is encoded a JSON object.

The JSON objects contains various name/value pairs, depending which content should be encoded:

* ''"id"'': the [[zettel identifier|00001006050000]], as a string.
* ''"url"'': the base URL to the zettel, as a string.
* ''"meta"'': the metadata of the zettel, as an JSON object. See below for details.
* ''"encoding"'' and ''"content"'': the actual content of the zettel. See below for details.

''"id"'' and ''"url"'' are always sent to the client.
It depends on the value of the required [[zettel part|00001012920800]], whether ''"meta"'' or ''"content"'' or both are sent.

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

* [[//z/00001012920501?_part=id]],
* [[//z/00001012920501?_part=zettel]],
* [[//z/00001012920501?_part=meta]],
* [[//z/00001012920501?_part=content]].

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

=== Metadata
This ia a JSON object, that maps [[metadata keys|00001006010000]] to their values.
Their values are encoded as strings, even if they contain a number (or something else).

You can always expect the keys ''"title"'', ''"tags"'', ''"syntax"'', and ''"role"'', together with their values.
The Zettelstore provides default values for these values, if they are not set for a zettel.

There is a list of [[supported metadata keys|00001006020000]].

=== Content
When the content is text-only, it is encoded as a plain string with an ''"encoding"'' value of ''""'' (empty string).

If the content contains binary content:

* ''"encoding"'' specifies the string encoding of the content.
  Currently, only the value ''"base64"'' is supported (as described in [[RFC4648, section 4|https://tools.ietf.org/html/rfc4648#section-4]].
* ''"value"'' is a string that contains the encoded binary content.

For example, if the content just consists of three zero bytes, it will be encoded as ``(...),"encoding":"base64","value":"AAAA"``{=json}.

Deleted docs/manual/00001012920503.zettel.

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



















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920503
title: DJSON Format
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

A zettel representation that allows to process the syntactic structure of a zettel.
It is a JSON-based encoding format, but different to [[json|00001012920501]].

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

* [[../z/00001012920503?_format=djson&_part=id]],
* [[../z/00001012920503?_format=djson&_part=zettel]],
* [[../z/00001012920503?_format=djson&_part=meta]],
* [[../z/00001012920503?_format=djson&_part=content]].

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

TODO: detailed description.

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 Format
title: HTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual
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.

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

Changes to docs/manual/00001012920513.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
14
15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30

-
+
+


-
+
+

-
+
-
+

-
+

+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
id: 00001012920513
title: Native Format
title: Markdown Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual
created: 20221107183011
modified: 20221107185130

A zettel representation shows the structure of a zettel in a more user-friendly way.
A zettel representation that tries to recreate a [[Markdown|00001008010500]] representation of the zettel.
Mostly used for debugging.
Useful if you want to convert [[other markup languages|00001008000000]] to Markdown (e.g. [[Zettelmarkup|00001007000000]]).

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

Please note that many elements of Zettelmarkup cannot be encoded in Markdown / CommonMark.
Examples are:
* [[Description lists|00001007030100]]
* [[Verse blocks|00001007030700]]
* [[Region blocks|00001007030800]]
* [[Comment blocks|00001007030900]] (and inline comments)
* [[Evaluation blocks|00001007031300]]
* [[Math-mode blocks|00001007031400]]
* [[Tables|00001007031000]]
* Most [[text formatting|00001007040100]] elements (except emphasis and quotation)
* Most [[literal-like formatting|00001007040200]] (except literal text / code spans)
Some elements are restricted, e.g. [[quotation lists|00001007030200]] are only supported as a top-level element.
TODO: formal description

Restricted and unsupported elements are not encoded.
They will not appear on the encoded output.

Maybe in the future, ignored elements may be shown as an HTML-like comment.

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




18
19
20


-
+
+


-
+
+

-
+

-
+
+

+
+
-
+

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

A zettel representation as it was loaded from the zettel content.
A zettel representation that is a [[s-expression|00001012930000]] (also known as symbolic expression).

Often used to have access to the bytes of an image.
It is (relatively) easy to parse and contain all relevant information of a zettel, metadata and content.
For example, take a look at the Sz encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920516?enc=sz&part=zettel]],
* [[//z/00001012920516?enc=sz&part=meta]],
If transferred via HTTP, the content type will depend on various factors:
* [[//z/00001012920516?enc=sz&part=content]].

* If the whole zettel should be transferred and it contains textual data only, ''text/plain'' will be used.
Otherwise the content type of the whole zettel will be ''application/octet-stream''.
* If just metadata is transferred, ''text/plain'' will be used.
* If just the content has to be transferred, the content type depends on the actual dats.
Some zettel describe the [[Sz encoding|00001012931000]] in a more detailed way.

If transferred via HTTP, the content type will be ''text/plain''.
  A PNG image will be transferred as ''image/png'', HTML content as ''text/html'', and so on.

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 Format
title: Text Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual
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, spearated by space character.
Every line may contain zero, one, or more words, separated by space character.

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

Changes to docs/manual/00001012920522.zettel.

1
2


3
4
5

6
7
8

9
10
1

2
3
4
5

6
7
8

9
10
11

-
+
+


-
+


-
+


id: 00001012920522
title: Zmk Format
title: Zmk Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual
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).
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).

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





+


-
+


-
+


-
+


-
+


-
-
-
id: 00001012920800
title: Values to specify zettel parts
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual
modified: 20220214175335

When working with [[zettel|00001006000000]], you could work with the whole zettel, with its metadata, or with its content:
; [!zettel]''zettel''
; [!zettel|''zettel'']
: Specifies that you work with a zettel as a whole.
  Contains identifier, metadata, and content of a zettel.
; [!meta]''meta''
; [!meta|''meta'']
: Specifies that you only want to cope with the metadata of a zettel.
  Contains identifier and metadata of a zettel.
; [!content]''content''
; [!content|''content'']
: Specifies that you are only interested in the zettel content.
  Contains identifier and content of a zettel.
; [!id]''id''
: States that you just want the zettel identifier and the URL of the zettel.
  Only valid for JSON-based encoding formats[^[[json|00001012920501]] and [[djson|00001012920503]].].

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 [[authentiction process|00001012050200]] was successful, an access token with some additional data is returned.
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.

Added 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220201173115
modified: 20230807164817

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
| 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.
# If it is greater than zero, move to step 1.

As an example, let's assume a rights value of 42:
# The first right is the right to delete a zettel.
  The new value of the rights value is now 10 (42-32).
# The next right is the right to update a zettel (16 > 10, but 8 < 10).
  The new value of the rights value is now 2 (10-8).
# The last right is the right to create a new zettel.
  The rights value is now zero, the algorithm ends.

In practice, not every rights value will occur.
A Zettelstore in [[read-only mode|00001010000000#read-only]] will always return the value 4.
Similar, a Zettelstore that you started with a [[double-click|00001003000000]] will return either the value ""6"" (reading and updating) or the value ""62"" (all operations are allowed).

If you have added an additional [[user|00001010040200]] to your Zettelstore, this might change.
The access rights are calculated depending on [[enabled authentication|00001010040100]], on the [[user role|00001010070300]] of the current user, on [[visibility rules|00001010070200]] for a given zettel and on the [[read-only status|00001006020400]] for the zettel.

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

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

Internally, lists are composed of __cells__.
A cell allows to store two values.
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://zettelstore.de/sx]] (""Symbolic eXPression Framework"") to implement symbolic expression.
  The project page might contain additional information about the full syntax.

  Zettelstore only uses lists, numbers, string, and symbols to represent zettel.

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

=== ''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 ore 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 break are transformed into hard line breaks to save the structure of the verse / poem.
Attributes may further specify something.
The inline typically describes author / source of the verse.

=== ''VERBATIM-*''
The following lists specifies some literal text of more than one line.
The structure is always the same, the initial symbol denotes the actual usage.
The content is encoded as a string, most likely to contain control characters that signals the end of a line.

:::syntax
__CommentVerbatim__ **=** ''(VERBATIM-COMMENT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as an internal comment not to be interpreted further.

:::syntax
__EvalVerbatim__ **=** ''(VERBATIM-EVAL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be evaluated by an (external) software to produce some derived content.

:::syntax
__HTMLVerbatim__ **=** ''(VERBATIM-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains HTML code.

:::syntax
__MathVerbatim__ **=** ''(VERBATIM-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as special code to be interpreted as mathematical formulas.

:::syntax
__CodeVerbatim__ **=** ''(VERBATIM-CODE'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (executable) code.

:::syntax
__ZettelVerbatim__ **=** ''(VERBATIM-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (nested) zettel content.

=== ''BLOB''
:::syntax
__BLOB__ **=** ''(BLOB'' ''('' [[__InlineElement__|00001012931600]] &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
210
211
212
213
214
215
216
217
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20240122122448

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, typically a word.

=== ''SPACE''
:::syntax
__Space__ **=** ''(SPACE'' **[** String **]** '')''.
:::
Specifies some space, typically white space.
If the string is not given it is assumed to be ''" "'' (one space character).
Otherwise it contains the space characters.

=== ''SOFT''
:::syntax
__Soft__ **=** ''(SOFT)''.
:::
Denotes a soft line break.
It is typically translated into a space character, but signals the point in the textual content, where a line break occurred.

=== ''HARD''
:::syntax
__Hard__ **=** ''(HARD)''.
:::
Specifies a hard line break, i.e. the user wants to have a line break here.

=== ''LINK-*''
The following lists specify various links, based on the full reference.
They all have the same structure, with a trailing sequence of __InlineElements__ that contain the linked text.

:::syntax
__InvalidLink__ **=** ''(LINK-INVALID'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &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 further encoded.
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 fto 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.

Added docs/manual/00001017000000.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: 00001017000000
title: Tips and Tricks
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20220803170112
modified: 20231012154803

=== 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.
   Let's assume this zettel receives the zettel identifier ''20220803182600''.
*# Create the zettel that should serve as the starting zettel for your users.
   It must have syntax [[Zettelmarkup|00001008000000#zmk]], i.e. the syntax metadata must be set to ''zmk''.
   If needed, set the runtime configuration [[''home-zettel|00001004020000#home-zettel]] to the value of the identifier of this zettel.
*# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel).
* **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user.
  In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel.
* **Solution 2:** Set a user-specific value by adding metadata ''home-zettel'' to the [[user zettel|00001010040200]].
* **Discussion:** A value for ''home-zettel'' is first searched in the user zettel of the current authenticated user.
  Only if it is not found, the value is looked up in the runtime configuration zettel.
  If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel.

=== Role-specific Layout of Zettel in Web User Interface (WebUI)
[!role-css]
* **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]].
  For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel.
  Or you want a multi-column layout.
* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Sxn Start Code|00000000019000]]"" (its identifier is ''00000000019000'').
  This zettel is the starting point for Sxn code, where you can place a definition for a variable named ""CSS-ROLE-map"".

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

=== 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''.
  The ""''~''"" is a shortcut and specifies your home folder.

  Unfortunately, Zettelstore does not yet support this shortcut.
  Therefore you must replace it with the absolute name of your home folder.
  In addition, a space character is not allowed in an URI.
  You have to replace it with the sequence ""''%20''"".

  Let us assume, that you stored your zettel box inside the folder ""zettel"", which is located top-level in your iCloud folder.
  In this case, you must specify the following box URI within the startup configuration: ''box-uri-1: dir:///Users/USERNAME/Library/Mobile%20Documents/com~apple~CloudDocs/zettel'', replacing ''USERNAME'' with the username of that specific computer (and assuming you want to use it as the first box).
* **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.
  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.
  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.

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001018000000
title: Troubleshooting
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211027105921
modified: 20240221134749

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.

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

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

Added docs/manual/20231128184200.zettel.








1
2
3
4
5
6
7
+
+
+
+
+
+
+
id: 20231128184200
title: manual
role: role
syntax: zmk
created: 20231128184200

Zettel with the role ""manual"" contain the manual of the zettelstore.

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










































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

// Package domain provides domain specific types, constants, and functions.
package domain

import "unicode/utf8"

// Content is just the uninterpreted content of a zettel.
type Content string

// NewContent creates a new content from a string.
func NewContent(s string) Content { return Content(s) }

// AsString returns the content itself is a string.
func (zc Content) AsString() string { return string(zc) }

// AsBytes returns the content itself is a byte slice.
func (zc Content) AsBytes() []byte { return []byte(zc) }

// 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 {
	s := string(zc)
	if !utf8.ValidString(s) {
		return true
	}
	l := len(s)
	for i := 0; i < l; i++ {
		if s[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




































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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) {
	td := []struct {
		s   string
		exp bool
	}{
		{"abc", false},
		{"äöü", false},
		{"", false},
		{string([]byte{0}), true},
	}
	for i, tc := range td {
		content := domain.NewContent(tc.s)
		got := content.IsBinary()
		if got != tc.exp {
			t.Errorf("TC=%d: expected %v, got %v", i, 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











































































































































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

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (
	"strconv"
	"time"
)

// 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.
// Note: if you change some values, ensure that you also change them in the
//       constant place. They are mentioned there literally, because these
//       constants are not available there.
const (
	Invalid = Zid(0) // Invalid is a Zid that will never be valid

	// System zettel
	VersionZid              = Zid(1)
	HostZid                 = Zid(2)
	OperatingSystemZid      = Zid(3)
	LicenseZid              = Zid(4)
	AuthorsZid              = Zid(5)
	DependenciesZid         = Zid(6)
	PlaceManagerZid         = Zid(20)
	MetadataKeyZid          = Zid(90)
	StartupConfigurationZid = Zid(96)
	ConfigurationZid        = Zid(100)

	// WebUI HTML templates are in the range 10000..19999
	BaseTemplateZid    = Zid(10100)
	LoginTemplateZid   = Zid(10200)
	ListTemplateZid    = Zid(10300)
	ZettelTemplateZid  = Zid(10401)
	InfoTemplateZid    = Zid(10402)
	FormTemplateZid    = Zid(10403)
	RenameTemplateZid  = Zid(10404)
	DeleteTemplateZid  = Zid(10405)
	ContextTemplateZid = Zid(10406)
	RolesTemplateZid   = Zid(10500)
	TagsTemplateZid    = Zid(10600)
	ErrorTemplateZid   = Zid(10700)

	// WebUI CSS zettel are in the range 20000..29999
	BaseCSSZid = Zid(20001)

	// WebUI JS zettel are in the range 30000..39999

	// WebUI image zettel are in the range 40000..49999
	EmojiZid = Zid(40001)

	// Range 90000...99999 is reserved for zettel templates
	TOCNewTemplateZid    = Zid(90000)
	TemplateNewZettelZid = Zid(90001)
	TemplateNewUserZid   = Zid(90002)

	DefaultHomeZid = Zid(10000000000)
)

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
}

const digits = "0123456789"

// String converts the zettel identification to a string of 14 digits.
// Only defined for valid ids.
func (zid Zid) String() string {
	return string(zid.Bytes())
}

// Bytes converts the zettel identification to a byte slice of 14 digits.
// Only defined for valid ids.
func (zid Zid) Bytes() []byte {
	result := make([]byte, 14)
	for i := 13; i >= 0; i-- {
		result[i] = digits[zid%10]
		zid /= 10
	}
	return result
}

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

// New returns a new zettel id based on the current time.
func New(withSeconds bool) Zid {
	now := time.Now()
	var s string
	if withSeconds {
		s = now.Format("20060102150405")
	} 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


































































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

// Package id_test provides unit tests for testing zettel id specific functions.
package id_test

import (
	"testing"

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

func TestIsValid(t *testing.T) {
	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, sid, zid, 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)
		}
	}
}

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





























































































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

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

// Set is a set of zettel identifier
type Set map[Zid]bool

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

// AddSlice adds all identifier of the given slice to the set.
func (s Set) AddSlice(sl Slice) {
	for _, zid := range sl {
		s[zid] = true
	}
}

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

// Intersect 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.
func (s Set) Intersect(other Set) Set {
	if len(s) > len(other) {
		s, other = other, s
	}
	for zid, inSet := range s {
		if !inSet {
			delete(s, zid)
			continue
		}
		otherInSet, otherOk := other[zid]
		if !otherInSet || !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, inSet := range other {
		if inSet {
			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
























































































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

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id_test

import (
	"testing"

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

func TestSetSorted(t *testing.T) {
	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 TestSetIntersection(t *testing.T) {
	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, nil},
		{id.NewSet(1), id.NewSet(), nil},
		{id.NewSet(1), id.NewSet(2), 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.Intersect(tc.s2).Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
		got = id.NewSet(sl2...).Intersect(id.NewSet(sl1...)).Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got)
		}
	}
}

func TestSetRemove(t *testing.T) {
	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)
		}
	}
}

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





































































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

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (
	"sort"
	"strings"
)

// 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 sb strings.Builder
	for i, zid := range zs {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.WriteString(zid.String())
	}
	return sb.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




















































































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

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id_test

import (
	"testing"

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

func TestSliceSort(t *testing.T) {
	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) {
	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) {
	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) {
	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/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










































































































































































































































































































































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

// Package meta provides the domain specific type 'meta'.
package meta

import (
	"regexp"
	"sort"
	"strings"

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

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

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

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

// GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name.
func GetSortedKeyDescriptions() []*DescriptionKey {
	names := make([]string, 0, len(registeredKeys))
	for n := range registeredKeys {
		names = append(names, n)
	}
	sort.Strings(names)
	result := make([]*DescriptionKey, 0, len(names))
	for _, n := range names {
		result = append(result, registeredKeys[n])
	}
	return result
}

// Supported keys.
var (
	KeyID                = registerKey("id", TypeID, usageComputed, "")
	KeyTitle             = registerKey("title", TypeZettelmarkup, usageUser, "")
	KeyRole              = registerKey("role", TypeWord, usageUser, "")
	KeyTags              = registerKey("tags", TypeTagSet, usageUser, "")
	KeySyntax            = registerKey("syntax", TypeWord, usageUser, "")
	KeyBack              = registerKey("back", TypeIDSet, usageProperty, "")
	KeyBackward          = registerKey("backward", TypeIDSet, usageProperty, "")
	KeyCopyright         = registerKey("copyright", TypeString, usageUser, "")
	KeyCredential        = registerKey("credential", TypeCredential, usageUser, "")
	KeyDead              = registerKey("dead", TypeIDSet, usageProperty, "")
	KeyDefaultCopyright  = registerKey("default-copyright", TypeString, usageUser, "")
	KeyDefaultLang       = registerKey("default-lang", TypeWord, usageUser, "")
	KeyDefaultLicense    = registerKey("default-license", TypeEmpty, usageUser, "")
	KeyDefaultRole       = registerKey("default-role", TypeWord, usageUser, "")
	KeyDefaultSyntax     = registerKey("default-syntax", TypeWord, usageUser, "")
	KeyDefaultTitle      = registerKey("default-title", TypeZettelmarkup, usageUser, "")
	KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "")
	KeyDuplicates        = registerKey("duplicates", TypeBool, usageUser, "")
	KeyExpertMode        = registerKey("expert-mode", TypeBool, usageUser, "")
	KeyFolge             = registerKey("folge", TypeIDSet, usageProperty, "")
	KeyFooterHTML        = registerKey("footer-html", TypeString, usageUser, "")
	KeyForward           = registerKey("forward", TypeIDSet, usageProperty, "")
	KeyHomeZettel        = registerKey("home-zettel", TypeID, usageUser, "")
	KeyLang              = registerKey("lang", TypeWord, usageUser, "")
	KeyLicense           = registerKey("license", TypeEmpty, usageUser, "")
	KeyListPageSize      = registerKey("list-page-size", TypeNumber, usageUser, "")
	KeyMarkerExternal    = registerKey("marker-external", TypeEmpty, usageUser, "")
	KeyModified          = registerKey("modified", TypeTimestamp, usageComputed, "")
	KeyNoIndex           = registerKey("no-index", TypeBool, usageUser, "")
	KeyPrecursor         = registerKey("precursor", TypeIDSet, usageUser, KeyFolge)
	KeyPublished         = registerKey("published", TypeTimestamp, usageProperty, "")
	KeyReadOnly          = registerKey("read-only", TypeWord, usageUser, "")
	KeySiteName          = registerKey("site-name", TypeString, usageUser, "")
	KeyURL               = registerKey("url", TypeURL, usageUser, "")
	KeyUserID            = registerKey("user-id", TypeWord, usageUser, "")
	KeyUserRole          = registerKey("user-role", TypeWord, usageUser, "")
	KeyVisibility        = registerKey("visibility", TypeWord, usageUser, "")
	KeyYAMLHeader        = registerKey("yaml-header", TypeBool, usageUser, "")
	KeyZettelFileSyntax  = registerKey("zettel-file-syntax", TypeWordSet, usageUser, "")
)

// Important values for some keys.
const (
	ValueRoleConfiguration = "configuration"
	ValueRoleUser          = "user"
	ValueRoleZettel        = "zettel"
	ValueSyntaxNone        = "none"
	ValueSyntaxGif         = "gif"
	ValueSyntaxText        = "text"
	ValueSyntaxZmk         = "zmk"
	ValueTrue              = "true"
	ValueFalse             = "false"
	ValueLangEN            = "en"
	ValueUserRoleReader    = "reader"
	ValueUserRoleWriter    = "writer"
	ValueUserRoleOwner     = "owner"
	ValueVisibilityExpert  = "expert"
	ValueVisibilityOwner   = "owner"
	ValueVisibilityLogin   = "login"
	ValueVisibilityPublic  = "public"
)

// 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 meta-data
func New(zid id.Zid) *Meta {
	return &Meta{Zid: zid, pairs: make(map[string]string, 5)}
}

// 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{KeyTitle, KeyRole, KeyTags, KeySyntax}
var firstKeySet map[string]bool

func init() {
	firstKeySet = make(map[string]bool, len(firstKeys))
	for _, k := range firstKeys {
		firstKeySet[k] = true
	}
}

// Set stores the given string value under the given key.
func (m *Meta) Set(key, value string) {
	if key != KeyID {
		m.pairs[key] = trimValue(value)
	}
}

func trimValue(value string) string {
	return strings.TrimFunc(value, runes.IsSpace)
}

// Get retrieves the string value of a given key. The bool value signals,
// whether there was a value stored or not.
func (m *Meta) Get(key string) (string, bool) {
	if key == KeyID {
		return m.Zid.String(), true
	}
	value, ok := m.pairs[key]
	return value, ok
}

// GetDefault retrieves the string value of the given key. If no value was
// stored, the given default value is returned.
func (m *Meta) GetDefault(key, def string) string {
	if value, ok := m.Get(key); ok {
		return value
	}
	return def
}

// Pairs 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) Pairs(allowComputed bool) []Pair {
	return m.doPairs(true, allowComputed)
}

// PairsRest returns all key/values pairs stored, except the values with
// predefined keys. The pairs are ordered by key.
func (m *Meta) PairsRest(allowComputed bool) []Pair {
	return m.doPairs(false, allowComputed)
}

func (m *Meta) doPairs(first, allowComputed bool) []Pair {
	result := make([]Pair, 0, len(m.pairs))
	if first {
		for _, key := range firstKeys {
			if value, ok := m.pairs[key]; ok {
				result = append(result, Pair{key, value})
			}
		}
	}

	keys := make([]string, 0, len(m.pairs)-len(result))
	for k := range m.pairs {
		if !firstKeySet[k] && (allowComputed || !IsComputed(k)) {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)

	for _, k := range keys {
		result = append(result, Pair{k, m.pairs[k]})
	}
	return result
}

// Delete removes a key from the data.
func (m *Meta) Delete(key string) {
	if key != 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(map[string]bool, len(m.pairs))
	for k, v := range m.pairs {
		tested[k] = true
		if !equalValue(k, v, o, allowComputed) {
			return false
		}
	}
	for k, v := range o.pairs {
		if !tested[k] && !equalValue(k, v, m, allowComputed) {
			return false
		}
	}
	return true
}

func equalValue(key, val string, other *Meta, allowComputed bool) bool {
	if allowComputed || !IsComputed(key) {
		if valO, ok := other.pairs[key]; !ok || val != valO {
			return false
		}
	}
	return true
}

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







































































































































































































































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

// Package meta provides the domain specific type 'meta'.
package meta

import (
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	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) {
	m := New(testID)
	if got, ok := m.Get(KeyTitle); ok || got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	addToMeta(m, KeyTitle, " ")
	if got, ok := m.Get(KeyTitle); ok || got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	const st = "A simple text"
	addToMeta(m, KeyTitle, " "+st+"  ")
	if got, ok := m.Get(KeyTitle); !ok || got != st {
		t.Errorf("Title is not %q, but %q", st, got)
	}
	addToMeta(m, KeyTitle, "  "+st+"\t")
	const exp = st + " " + st
	if got, ok := m.Get(KeyTitle); !ok || got != exp {
		t.Errorf("Title is not %q, but %q", exp, got)
	}

	m = New(testID)
	const at = "A Title"
	addToMeta(m, KeyTitle, at)
	addToMeta(m, KeyTitle, " ")
	if got, ok := m.Get(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) {
	m := New(testID)
	checkSet(t, []string{}, m, KeyTags)

	addToMeta(m, KeyTags, "")
	checkSet(t, []string{}, m, KeyTags)

	addToMeta(m, KeyTags, "  #t1 #t2  #t3 #t4  ")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags)

	addToMeta(m, KeyTags, "#t5")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags)

	addToMeta(m, KeyTags, "t6")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags)
}

func TestSyntax(t *testing.T) {
	m := New(testID)
	if got, ok := m.Get(KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, KeySyntax, " ")
	if got, _ := m.Get(KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, KeySyntax, "MarkDown")
	const exp = "markdown"
	if got, ok := m.Get(KeySyntax); !ok || got != exp {
		t.Errorf("Syntax is not %q, but %q", exp, got)
	}
	addToMeta(m, KeySyntax, " ")
	if got, _ := m.Get(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) {
	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(true))
	addToMeta(m, "", "d0")
	checkHeader(t, exp, m.Pairs(true))
	addToMeta(m, "h3", "")
	exp["h3"] = ""
	checkHeader(t, exp, m.Pairs(true))
	addToMeta(m, "h3", "  ")
	checkHeader(t, exp, m.Pairs(true))
	addToMeta(m, "h4", " ")
	exp["h4"] = ""
	checkHeader(t, exp, m.Pairs(true))
}

func TestDelete(t *testing.T) {
	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) {
	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{KeyFolge, "0"}, nil, true, false},
		{[]string{KeyFolge, "0"}, nil, false, true},
		{[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, true, true},
		{[]string{KeyFolge, "0"}, []string{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
}

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
178
179
180
181
182
183
184
185
186
187



























































































































































































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

// Package meta provides the domain specific type 'meta'.
package meta

import (
	"sort"
	"strings"

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

// 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 string
	for {
		skipSpace(inp)
		pos = inp.Pos
		skipToEOL(inp)
		val += inp.Src[pos:inp.Pos]
		inp.EatEOL()
		if !runes.IsSpace(inp.Ch) {
			break
		}
		val += " "
	}
	addToMeta(m, key, val)
}

func skipSpace(inp *input.Input) {
	for runes.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 map[string]bool, elems []string, useElem predValidElem) {
	for _, s := range elems {
		if len(s) > 0 && useElem(s) {
			set[s] = true
		}
	}
}

func addSet(m *Meta, key, val string, useElem predValidElem) {
	newElems := strings.Fields(val)
	oldElems, ok := m.GetList(key)
	if !ok {
		oldElems = nil
	}

	set := make(map[string]bool, len(newElems)+len(oldElems))
	addToSet(set, newElems, useElem)
	if len(set) == 0 {
		// Nothing to add. Maybe because of filtered elements.
		return
	}
	addToSet(set, oldElems, useElem)

	resultList := make([]string, 0, len(set))
	for tag := range set {
		resultList = append(resultList, tag)
	}
	sort.Strings(resultList)
	m.SetList(key, resultList)
}

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 "", KeyID:
		// Empty key and 'id' key will be ignored
		return
	}

	switch Type(key) {
	case TypeString, TypeZettelmarkup:
		if v != "" {
			addData(m, key, v)
		}
	case TypeTagSet:
		addSet(m, key, v, func(s string) bool { return s[0] == '#' })
	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


































































































































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

// Package meta_test provides tests for the domain specific type 'meta'.
package meta_test

import (
	"testing"

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

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput(src))
}

func TestEmpty(t *testing.T) {
	m := parseMetaStr("")
	if got, ok := m.Get(meta.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 {
		t.Errorf("Tags are not nil, but %v", got)
	}
}

func TestTitle(t *testing.T) {
	td := []struct{ s, e string }{
		{meta.KeyTitle + ": a title", "a title"},
		{meta.KeyTitle + ": a\n\t title", "a title"},
		{meta.KeyTitle + ": a\n\t title\r\n  x", "a title x"},
		{meta.KeyTitle + " AbC", "AbC"},
		{meta.KeyTitle + " AbC\n ded", "AbC ded"},
		{meta.KeyTitle + ": o\ntitle: p", "o p"},
		{meta.KeyTitle + ": O\n\ntitle: P", "O"},
		{meta.KeyTitle + ": b\r\ntitle: c", "b c"},
		{meta.KeyTitle + ": B\r\n\r\ntitle: C", "B"},
		{meta.KeyTitle + ": r\rtitle: q", "r q"},
		{meta.KeyTitle + ": R\r\rtitle: Q", "R"},
	}
	for i, tc := range td {
		m := parseMetaStr(tc.s)
		if got, ok := m.Get(meta.KeyTitle); !ok || got != tc.e {
			t.Log(m)
			t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got)
		}
	}

	m := parseMetaStr(meta.KeyTitle + ": ")
	if title, ok := m.Get(meta.KeyTitle); ok {
		t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m)
	}
}

func TestNewFromInput(t *testing.T) {
	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"}}},
	}
	for i, tc := range testcases {
		meta := parseMetaStr(tc.input)
		if got := meta.Pairs(true); !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("---\na:b\n---\nX")
	m := meta.NewFromInput(testID, inp)
	exp := []meta.Pair{{"a", "b"}}
	if got := m.Pairs(true); !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) {
	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(meta.KeyPrecursor + ": " + tc.inp)
		if got, ok := m.Get(meta.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
















































































































































































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

// Package meta provides the domain specific type 'meta'.
package meta

import (
	"strconv"
	"strings"
	"time"
)

// 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 (
	TypeBool         = registerType("Boolean", false)
	TypeCredential   = registerType("Credential", false)
	TypeEmpty        = registerType("EString", false)
	TypeID           = registerType("Identifier", false)
	TypeIDSet        = registerType("IdentifierSet", true)
	TypeNumber       = registerType("Number", false)
	TypeString       = registerType("String", false)
	TypeTagSet       = registerType("TagSet", true)
	TypeTimestamp    = registerType("Timestamp", false)
	TypeURL          = registerType("URL", false)
	TypeUnknown      = registerType("Unknown", false)
	TypeWord         = registerType("Word", false)
	TypeWordSet      = registerType("WordSet", true)
	TypeZettelmarkup = registerType("Zettelmarkup", false)
)

// Type returns a type hint for the given key. If no type hint is specified,
// TypeUnknown is returned.
func (m *Meta) Type(key string) *DescriptionType {
	return Type(key)
}

// Type returns a type hint for the given key. If no type hint is specified,
// TypeUnknown is returned.
func Type(key string) *DescriptionType {
	if k, ok := registeredKeys[key]; ok {
		return k.Type
	}
	return TypeUnknown
}

// SetList stores the given string list value under the given key.
func (m *Meta) SetList(key string, values []string) {
	if key != KeyID {
		for i, val := range values {
			values[i] = trimValue(val)
		}
		m.pairs[key] = strings.Join(values, " ")
	}
}

// CleanTag removes the number charachter ('#') from a tag value.
func CleanTag(tag string) string {
	if len(tag) > 1 && tag[0] == '#' {
		return tag[1:]
	}
	return tag
}

// SetNow stores the current timestamp under the given key.
func (m *Meta) SetNow(key string) {
	m.Set(key, time.Now().Format("20060102150405"))
}

// 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("20060102150405", 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) {
	tags, ok := m.GetList(key)
	if !ok {
		return nil, false
	}
	for i, tag := range tags {
		tags[i] = CleanTag(tag)
	}
	return tags, len(tags) > 0
}

// 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) (int, bool) {
	if value, ok := m.Get(key); ok {
		if num, err := strconv.Atoi(value); err == nil {
			return num, true
		}
	}
	return 0, false
}

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































































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

// Package meta_test provides tests for the domain specific type 'meta'.
package meta_test

import (
	"strconv"
	"testing"
	"time"

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

func TestNow(t *testing.T) {
	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) {
	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


































































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

// Package meta provides the domain specific type 'meta'.
package meta

// Visibility enumerates the variations of the 'visibility' meta key.
type Visibility int

// Supported values for visibility.
const (
	_ Visibility = iota
	VisibilityUnknown
	VisibilityPublic
	VisibilityLogin
	VisibilityOwner
	VisibilityExpert
)

var visMap = map[string]Visibility{
	ValueVisibilityPublic: VisibilityPublic,
	ValueVisibilityLogin:  VisibilityLogin,
	ValueVisibilityOwner:  VisibilityOwner,
	ValueVisibilityExpert: VisibilityExpert,
}

// GetVisibility returns the visibility value of the given string
func GetVisibility(val string) Visibility {
	if vis, ok := visMap[val]; ok {
		return vis
	}
	return VisibilityUnknown
}

// UserRole enumerates the supported values of meta key 'user-role'.
type UserRole int

// Supported values for user roles.
const (
	_ UserRole = iota
	UserRoleUnknown
	UserRoleReader
	UserRoleWriter
	UserRoleOwner
)

var urMap = map[string]UserRole{
	ValueUserRoleReader: UserRoleReader,
	ValueUserRoleWriter: UserRoleWriter,
	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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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"
	"io"
)

// Write writes a zettel meta to a writer.
func (m *Meta) Write(w io.Writer, allowComputed bool) (int, error) {
	var buf bytes.Buffer
	for _, p := range m.Pairs(allowComputed) {
		buf.WriteString(p.Key)
		buf.WriteString(": ")
		buf.WriteString(p.Value)
		buf.WriteByte('\n')
	}
	return w.Write(buf.Bytes())
}

var (
	newline = []byte{'\n'}
	yamlSep = []byte{'-', '-', '-', '\n'}
)

// WriteAsHeader writes the zettel meta to the writer, plus the separators
func (m *Meta) WriteAsHeader(w io.Writer, allowComputed bool) (int, error) {
	var lb, lc, la int
	var err error

	if m.YamlSep {
		lb, err = w.Write(yamlSep)
		if err != nil {
			return lb, err
		}
	}
	lc, err = m.Write(w, allowComputed)
	if err != nil {
		return lb + lc, err
	}
	if m.YamlSep {
		la, err = w.Write(yamlSep)
	} else {
		la, err = w.Write(newline)
	}
	return lb + lc + la, err
}

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
























































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

// Package meta_test provides tests for the domain specific type 'meta'.
package meta_test

import (
	"strings"
	"testing"

	"zettelstore.de/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(meta.KeyTitle, title)
	}
	if tags != nil {
		m.Set(meta.KeyTags, strings.Join(tags, " "))
	}
	if syntax != "" {
		m.Set(meta.KeySyntax, syntax)
	}
	return m
}
func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
	t.Helper()
	sb := strings.Builder{}
	m.Write(&sb, true)
	if got := sb.String(); got != expected {
		t.Errorf("\nExp: %q\ngot: %q", expected, got)
	}
}

func TestWriteMeta(t *testing.T) {
	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



























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

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

// Equal compares two zettel for equality.
func (z Zettel) Equal(o Zettel, allowComputed bool) bool {
	return z.Meta.Equal(o.Meta, allowComputed) && z.Content == o.Content
}

Deleted encoder/buffer.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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







































































































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

// Package encoder provides a generic interface to encode the abstract syntax
// tree into some text form.
package encoder

import (
	"encoding/base64"
	"io"
)

// BufWriter is a specialized buffered writer for encoding zettel.
type BufWriter struct {
	w      io.Writer // The io.Writer to write to
	err    error     // Collect error
	length int       // Sum length
	buf    []byte    // Buffer to collect bytes
}

// NewBufWriter creates a new BufWriter
func NewBufWriter(w io.Writer) BufWriter {
	return BufWriter{w: w, buf: make([]byte, 0, 4096)}
}

// Write writes the contents of p into the buffer.
func (w *BufWriter) Write(p []byte) (int, error) {
	if w.err == nil {
		w.buf = append(w.buf, p...)
		if len(w.buf) > 2048 {
			w.flush()
			if w.err != nil {
				return 0, w.err
			}
		}
		return len(p), nil
	}
	return 0, w.err
}

// WriteString writes the contents of s into the buffer.
func (w *BufWriter) WriteString(s string) (int, error) {
	return w.Write([]byte(s))
}

// WriteStrings writes the contents of sl into the buffer.
func (w *BufWriter) WriteStrings(sl ...string) {
	for _, s := range sl {
		w.WriteString(s)
	}
}

// WriteByte writes the content of b into the buffer.
func (w *BufWriter) WriteByte(b byte) error {
	w.buf = append(w.buf, b)
	return nil
}

// WriteBytes writes the content of bs into the buffer.
func (w *BufWriter) WriteBytes(bs ...byte) {
	w.buf = append(w.buf, bs...)
}

// WriteBase64 writes the content of p into the buffer, encoded with base64.
func (w *BufWriter) WriteBase64(p []byte) {
	if w.err == nil {
		w.flush()
	}
	if w.err == nil {
		encoder := base64.NewEncoder(base64.StdEncoding, w.w)
		length, err := encoder.Write(p)
		w.length += length
		err1 := encoder.Close()
		if err == nil {
			w.err = err1
		} else {
			w.err = err
		}
	}
}

// Flush writes any buffered data to the underlying io.Writer. It returns the
// number of bytes written and an error if something went wrong.
func (w *BufWriter) Flush() (int, error) {
	if w.err == nil {
		w.flush()
	}
	return w.length, w.err
}

func (w *BufWriter) flush() {
	length, err := w.w.Write(w.buf)
	w.buf = w.buf[:0]
	w.length += length
	w.err = err
}

Deleted encoder/encfun/encfun.go.

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

// Package encfun provides some helper function to work with encodings.
package encfun

import (
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
)

// MetaAsInlineSlice returns the value of the given metadata key as an inlince slice.
func MetaAsInlineSlice(m *meta.Meta, key string) ast.InlineSlice {
	return parser.ParseMetadata(m.GetDefault(key, ""))
}

// MetaAsText returns the value of given metadata as text.
func MetaAsText(m *meta.Meta, key string) string {
	textEncoder := encoder.Create("text", nil)
	var sb strings.Builder
	_, err := textEncoder.WriteInlines(&sb, MetaAsInlineSlice(m, key))
	if err == nil {
		return sb.String()
	}
	return ""
}

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


32




33
34
35
36
37
38
39
40
41
42
43
44
45
46



47
48
49
50


51
52



53
54

55
56
57

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

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21


22
23
24

25
26
27
28
29


30
31
32


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50



51
52
53
54
55
56
57
58
59


60
61
62


63
64
65

66

67
68



69
70
71
72







73
74
75





76
77
78
79
80
81

82
83













-
+

-
+




+
+
+








+

-
-

+

-
+




-
-
+
+

-
-
+
+

+
+
+
+











-
-
-
+
+
+




+
+
-
-
+
+
+
-
-
+


-
+
-


-
-
-
+
+
+

-
-
-
-
-
-
-
+


-
-
-
-
-
+
+
+
+
+

-


-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package encoder provides a generic interface to encode the abstract syntax
// tree into some text form.
package encoder

import (
	"errors"
	"fmt"
	"io"
	"log"
	"sort"

	"zettelstore.de/client.fossil/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, bool) (int, error)
	WriteMeta(io.Writer, *meta.Meta) (int, error)
	WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error)
	WriteMeta(io.Writer, *meta.Meta, EvalMetaFunc) (int, error)
	WriteContent(io.Writer, *ast.ZettelNode) (int, error)
	WriteBlocks(io.Writer, ast.BlockSlice) (int, error)
	WriteInlines(io.Writer, ast.InlineSlice) (int, error)
	WriteBlocks(io.Writer, *ast.BlockSlice) (int, error)
	WriteInlines(io.Writer, *ast.InlineSlice) (int, error)
}

// EvalMetaFunc is a function that takes a string of metadata and returns
// a list of syntax elements.
type EvalMetaFunc func(string) ast.InlineSlice

// Some errors to signal when encoder methods are not implemented.
var (
	ErrNoWriteZettel  = errors.New("method WriteZettel is not implemented")
	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(format string, env *Environment) Encoder {
	if info, ok := registry[format]; ok {
		return info.Create(env)
func Create(enc api.EncodingEnum, params *CreateParameter) Encoder {
	if create, ok := registry[enc]; ok {
		return create(params)
	}
	return nil
}

// CreateFunc produces a new encoder.
type CreateFunc func(*CreateParameter) Encoder
// Info stores some data about an encoder.
type Info struct {

// CreateParameter contains values that are needed to create an encoder.
type CreateParameter struct {
	Create  func(*Environment) Encoder
	Default bool
	Lang string // default language
}

var registry = map[string]Info{}
var registry = map[api.EncodingEnum]CreateFunc{}
var defFormat string

// Register the encoder for later retrieval.
func Register(format string, info Info) {
	if _, ok := registry[format]; ok {
		log.Fatalf("Writer with format %q already registered", format)
func Register(enc api.EncodingEnum, create CreateFunc) {
	if _, ok := registry[enc]; ok {
		panic(fmt.Sprintf("Encoder %q already registered", enc))
	}
	if info.Default {
		if defFormat != "" && defFormat != format {
			log.Fatalf("Default format already set: %q, new format: %q", defFormat, format)
		}
		defFormat = format
	}
	registry[format] = info
	registry[enc] = create
}

// GetFormats returns all registered formats, ordered by format name.
func GetFormats() []string {
	result := make([]string, 0, len(registry))
	for format := range registry {
		result = append(result, format)
// GetEncodings returns all registered encodings, ordered by encoding value.
func GetEncodings() []api.EncodingEnum {
	result := make([]api.EncodingEnum, 0, len(registry))
	for enc := range registry {
		result = append(result, enc)
	}
	sort.Strings(result)
	return result
}

// GetDefaultFormat returns the format that should be used as default.
func GetDefaultFormat() string {
	if defFormat != "" {
		return defFormat
	}
	if _, ok := registry["json"]; ok {
		return "json"
	}
	log.Fatalf("No default format given")
	return ""
}

Added 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package encoder_test

import (
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/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
	expect expectMap
}

var pngTestCases = []blobTestCase{
	{
		descr: "Minimal PNG",
		blob: []byte{
			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{
			encoderHTML:  `<p><img alt="PNG" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="></p>`,
			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) {
	m := meta.New(id.Invalid)
	m.Set(api.KeyTitle, "PNG")
	for testNum, tc := range pngTestCases {
		inp := input.NewInput(tc.blob)
		pe := &peBlocks{bs: parser.ParseBlocks(inp, m, "png", config.NoHTML)}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, "???")
	}
}

Added 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
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-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European 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{
			encoderHTML:  "",
			encoderMD:    "",
			encoderSz:    `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world",
		zmk:   "Hello, world",
		expect: expectMap{
			encoderHTML:  "<p>Hello, world</p>",
			encoderMD:    "Hello, world",
			encoderSz:    `(BLOCK (PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`,
			encoderSHTML: `((p "Hello," " " "world"))`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple block comment",
		zmk:   "%%%\nNo\nrender\n%%%",
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(BLOCK (VERBATIM-COMMENT () "No\nrender"))`,
			encoderSHTML: `(())`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Rendered block comment",
		zmk:   "%%%{-}\nRender\n%%%",
		expect: expectMap{
			encoderHTML:  "<!--\nRender\n-->\n",
			encoderMD:    "",
			encoderSz:    `(BLOCK (VERBATIM-COMMENT (("-" . "")) "Render"))`,
			encoderSHTML: "((@@@ \"Render\"))",
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Heading",
		zmk:   `=== Top Job`,
		expect: expectMap{
			encoderHTML:  "<h2 id=\"top-job\">Top Job</h2>",
			encoderMD:    "# Top Job",
			encoderSz:    `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top") (SPACE) (TEXT "Job")))`,
			encoderSHTML: `((h2 (@ (id . "top-job")) "Top" " " "Job"))`,
			encoderText:  `Top Job`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List",
		zmk:   "* A\n* B\n* C",
		expect: expectMap{
			encoderHTML:  "<ul><li>A</li><li>B</li><li>C</li></ul>",
			encoderMD:    "* A\n* B\n* C",
			encoderSz:    `(BLOCK (UNORDERED (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`,
			encoderSHTML: `((ul (li "A") (li "B") (li "C")))`,
			encoderText:  "A\nB\nC",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested List",
		zmk:   "* T1\n** T2\n* T3\n** T4\n** T5\n* T6",
		expect: expectMap{
			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",
			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{
			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",
			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{
			encoderHTML:  "<hr>",
			encoderMD:    "---",
			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{
			encoderHTML:  "<p>Text *abc</p>",
			encoderMD:    "Text\n*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{
			encoderHTML:  "<p>Text</p><ol><li>abc</li></ol>",
			encoderMD:    "Text\n\n1. 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{
			encoderHTML:  "<blockquote>ToBeOrNotToBe</blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSz:    `(BLOCK (QUOTATION (INLINE (TEXT "ToBeOrNotToBe"))))`,
			encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`,
			encoderText:  "ToBeOrNotToBe",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Quote Block",
		zmk:   "<<<\nToBeOrNotToBe\n<<< Romeo Julia",
		expect: expectMap{
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo Julia</cite></blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSz:    `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) (TEXT "Romeo") (SPACE) (TEXT "Julia")))`,
			encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo" " " "Julia")))`,
			encoderText:  "ToBeOrNotToBe\nRomeo Julia",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quote Block with multiple paragraphs",
		zmk:   "<<<\nToBeOr\n\nNotToBe\n<<< Romeo",
		expect: expectMap{
			encoderHTML:  "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>",
			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{
			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:    "",
			encoderSz:    "(BLOCK (REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) (TEXT \"Author\")))",
			encoderSHTML: "((div (p \"A\" \"\u00a0\" \"line\" (br) \"\u00a0\u00a0\" \"another\" \"\u00a0\" \"line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0\" \"Spacy\" \"\u00a0\u00a0\" \"Para\") (cite \"Author\")))",
			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{
			encoderHTML:  "<div><p>A simple  span and much more</p></div>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more")))))`,
			encoderSHTML: `((div (p "A" " " "simple" " " " " "span" " " "and" " " "much" " " "more")))`,
			encoderText:  `A simple  span and much more`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code",
		zmk:   "```\nHello\nWorld\n```",
		expect: expectMap{
			encoderHTML:  "<pre><code>Hello\nWorld</code></pre>",
			encoderMD:    "    Hello\n    World",
			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{
			encoderHTML:  "<pre><code>Hello\u2423World</code></pre>",
			encoderMD:    "    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{
			encoderHTML:  "<pre><code class=\"zs-eval\">Hello\nWorld</code></pre>",
			encoderMD:    "",
			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{
			encoderHTML:  "<pre><code class=\"zs-math\">Hello\n\\LaTeX</code></pre>",
			encoderMD:    "",
			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{
			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") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with paragraphs as item",
		zmk:   "; Zettel\n: Paper\n\n  Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p><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")) (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with keys, but no descriptions",
		zmk:   "; K1\n: D11\n: D12\n; K2\n; K3\n: D31",
		expect: expectMap{
			encoderHTML:  "<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{
			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:    "",
			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{
			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>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`,
			encoderSHTML: `((table (thead (tr (td (@ (class . "right")) "h1") (td "h2") (td (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`,
			encoderText:  "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: `|=h1>|=h2|=h3:
|<c1|c2|c3
|f1|f2|=f3`,
		},
	},
	{
		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",
			encoderZmk:   useZmk,
		},
	},
	{
		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",
			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{
			encoderHTML:  `<p><img class="external" src="http://example.com/image" width="100px"></p>`,
			encoderMD:    "",
			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{
			// encoderHTML:  ``,
			encoderSz: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`,
			// encoderSHTML: ``,
			encoderText: "",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderHTML:  ``,
			encoderSz:    `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
}

// func TestEncoderBlock(t *testing.T) {
// 	executeTestCases(t, tcsBlock)
// }

Added 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
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-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European 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{
			encoderHTML:  "",
			encoderMD:    "",
			encoderSz:    `(INLINE)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world (inline)",
		zmk:   `Hello, world`,
		expect: expectMap{
			encoderHTML:  "Hello, world",
			encoderMD:    "Hello, world",
			encoderSz:    `(INLINE (TEXT "Hello,") (SPACE) (TEXT "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{
			encoderHTML:  "<em>emph</em>",
			encoderMD:    "*emph*",
			encoderSz:    `(INLINE (FORMAT-EMPH () (TEXT "emph")))`,
			encoderSHTML: `((em "emph"))`,
			encoderText:  "emph",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Strong formatting",
		zmk:   "**strong**",
		expect: expectMap{
			encoderHTML:  "<strong>strong</strong>",
			encoderMD:    "__strong__",
			encoderSz:    `(INLINE (FORMAT-STRONG () (TEXT "strong")))`,
			encoderSHTML: `((strong "strong"))`,
			encoderText:  "strong",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Insert formatting",
		zmk:   ">>insert>>",
		expect: expectMap{
			encoderHTML:  "<ins>insert</ins>",
			encoderMD:    "insert",
			encoderSz:    `(INLINE (FORMAT-INSERT () (TEXT "insert")))`,
			encoderSHTML: `((ins "insert"))`,
			encoderText:  "insert",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Delete formatting",
		zmk:   "~~delete~~",
		expect: expectMap{
			encoderHTML:  "<del>delete</del>",
			encoderMD:    "delete",
			encoderSz:    `(INLINE (FORMAT-DELETE () (TEXT "delete")))`,
			encoderSHTML: `((del "delete"))`,
			encoderText:  "delete",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Update formatting",
		zmk:   "~~old~~>>new>>",
		expect: expectMap{
			encoderHTML:  "<del>old</del><ins>new</ins>",
			encoderMD:    "oldnew",
			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{
			encoderHTML:  `<sup>superscript</sup>`,
			encoderMD:    "superscript",
			encoderSz:    `(INLINE (FORMAT-SUPER () (TEXT "superscript")))`,
			encoderSHTML: `((sup "superscript"))`,
			encoderText:  `superscript`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Subscript formatting",
		zmk:   ",,subscript,,",
		expect: expectMap{
			encoderHTML:  `<sub>subscript</sub>`,
			encoderMD:    "subscript",
			encoderSz:    `(INLINE (FORMAT-SUB () (TEXT "subscript")))`,
			encoderSHTML: `((sub "subscript"))`,
			encoderText:  `subscript`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderHTML:  "&ldquo;quotes&rdquo;",
			encoderMD:    "<q>quotes</q>",
			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{
			encoderHTML:  `<span lang="de">&bdquo;quotes&ldquo;</span>`,
			encoderMD:    "<q>quotes</q>",
			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:    "<q></q>",
			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:    "<q></q>",
			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:    "<q>say: <q>yes, <q>or?</q></q></q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "say:") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes,") (SPACE) (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:    "<q>yes</q> or <q>no</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (SPACE) (TEXT "or") (SPACE) (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{
			encoderHTML:  `<span>span</span>`,
			encoderMD:    "span",
			encoderSz:    `(INLINE (FORMAT-SPAN () (TEXT "span")))`,
			encoderSHTML: `((span "span"))`,
			encoderText:  `span`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Code formatting",
		zmk:   "``code``",
		expect: expectMap{
			encoderHTML:  `<code>code</code>`,
			encoderMD:    "`code`",
			encoderSz:    `(INLINE (LITERAL-CODE () "code"))`,
			encoderSHTML: `((code "code"))`,
			encoderText:  `code`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Code formatting with visible space",
		zmk:   "``x y``{-}",
		expect: expectMap{
			encoderHTML:  "<code>x\u2423y</code>",
			encoderMD:    "`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{
			encoderHTML:  "<code>&lt;script </code> abc",
			encoderMD:    "`<script ` abc",
			encoderSz:    `(INLINE (LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`,
			encoderSHTML: `((code "<script ") " " "abc")`,
			encoderText:  `<script  abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Input formatting",
		zmk:   `''input''`,
		expect: expectMap{
			encoderHTML:  `<kbd>input</kbd>`,
			encoderMD:    "input",
			encoderSz:    `(INLINE (LITERAL-INPUT () "input"))`,
			encoderSHTML: `((kbd "input"))`,
			encoderText:  `input`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Output formatting",
		zmk:   `==output==`,
		expect: expectMap{
			encoderHTML:  `<samp>output</samp>`,
			encoderMD:    "output",
			encoderSz:    `(INLINE (LITERAL-OUTPUT () "output"))`,
			encoderSHTML: `((samp "output"))`,
			encoderText:  `output`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Math formatting",
		zmk:   `$$\TeX$$`,
		expect: expectMap{
			encoderHTML:  `<code class="zs-math">\TeX</code>`,
			encoderMD:    "\\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{
			encoderHTML:  `<span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span>`,
			encoderMD:    "<q>abc</q>",
			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{
			encoderHTML:  `<span>Stern18</span>`, // TODO
			encoderMD:    "",
			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{
			encoderHTML:  `% comment`,
			encoderMD:    "% comment",
			encoderSz:    `(INLINE (TEXT "%") (SPACE) (TEXT "comment"))`,
			encoderSHTML: `("%" " " "comment")`,
			encoderText:  `% comment`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment (nogen HTML)",
		zmk:   `%% line comment`,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(INLINE (LITERAL-COMMENT () "line comment"))`,
			encoderSHTML: `(())`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment",
		zmk:   `%%{-} line comment`,
		expect: expectMap{
			encoderHTML:  `<!-- line comment -->`,
			encoderMD:    "",
			encoderSz:    `(INLINE (LITERAL-COMMENT (("-" . "")) "line comment"))`,
			encoderSHTML: `((@@ "line comment"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text",
		zmk:   `Text %%{-} comment`,
		expect: expectMap{
			encoderHTML:  `Text<!-- comment -->`,
			encoderMD:    "Text",
			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{
			encoderHTML:  `Text<!-- comment -&#45;> end -->`,
			encoderMD:    "Text",
			encoderSz:    `(INLINE (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment --> end"))`,
			encoderSHTML: `("Text" (@@ "comment --> end"))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple inline endnote",
		zmk:   `[^endnote]`,
		expect: expectMap{
			encoderHTML:  `<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup>`,
			encoderMD:    "",
			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{
			encoderHTML:  `<a id="mark"></a>`,
			encoderMD:    "",
			encoderSz:    `(INLINE (MARK "mark" "mark" "mark"))`,
			encoderSHTML: `((a (@ (id . "mark"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark with text",
		zmk:   `[!mark|with text]`,
		expect: expectMap{
			encoderHTML:  `<a id="mark">with text</a>`,
			encoderMD:    "with text",
			encoderSz:    `(INLINE (MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "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{
			encoderHTML:  `<a class="external" href="abc">abc</a>`,
			encoderMD:    "[abc](abc)",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "abc"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "abc")) "abc"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple URL",
		zmk:   `[[https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<a class="external" href="https://zettelstore.de">https://zettelstore.de</a>`,
			encoderMD:    "<https://zettelstore.de>",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "https://zettelstore.de"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "URL with Text",
		zmk:   `[[Home|https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<a class="external" href="https://zettelstore.de">Home</a>`,
			encoderMD:    "[Home](https://zettelstore.de)",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "Home"))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,
		expect: expectMap{
			encoderHTML:  `<a href="00000000000100">00000000000100</a>`,
			encoderMD:    "[00000000000100](00000000000100)",
			encoderSz:    `(INLINE (LINK-ZETTEL () "00000000000100"))`,
			encoderSHTML: `((a (@ (href . "00000000000100")) "00000000000100"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Zettel ID with Text",
		zmk:   `[[Config|00000000000100]]`,
		expect: expectMap{
			encoderHTML:  `<a href="00000000000100">Config</a>`,
			encoderMD:    "[Config](00000000000100)",
			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{
			encoderHTML:  `<a href="00000000000100#frag">00000000000100#frag</a>`,
			encoderMD:    "[00000000000100#frag](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{
			encoderHTML:  `<a href="00000000000100#frag">Config</a>`,
			encoderMD:    "[Config](00000000000100#frag)",
			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{
			encoderHTML:  `<a href="#frag">#frag</a>`,
			encoderMD:    "[#frag](#frag)",
			encoderSz:    `(INLINE (LINK-SELF () "#frag"))`,
			encoderSHTML: `((a (@ (href . "#frag")) "#frag"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Hosted link",
		zmk:   `[[H|/hosted]]`,
		expect: expectMap{
			encoderHTML:  `<a href="/hosted">H</a>`,
			encoderMD:    "[H](/hosted)",
			encoderSz:    `(INLINE (LINK-HOSTED () "/hosted" (TEXT "H")))`,
			encoderSHTML: `((a (@ (href . "/hosted")) "H"))`,
			encoderText:  `H`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Based link",
		zmk:   `[[B|//based]]`,
		expect: expectMap{
			encoderHTML:  `<a href="/based">B</a>`,
			encoderMD:    "[B](/based)",
			encoderSz:    `(INLINE (LINK-BASED () "/based" (TEXT "B")))`,
			encoderText:  `B`,
			encoderSHTML: `((a (@ (href . "/based")) "B"))`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Relative link",
		zmk:   `[[R|../relative]]`,
		expect: expectMap{
			encoderHTML:  `<a href="../relative">R</a>`,
			encoderMD:    "[R](../relative)",
			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{
			encoderHTML:  `<a href="?q=title%3Asyntax">title:syntax</a>`,
			encoderMD:    "",
			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{
			encoderHTML:  `<a href="?q=title%3Asyntax">Q</a>`,
			encoderMD:    "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{
			encoderHTML:  `<img src="abc">`,
			encoderMD:    "![abc](abc)",
			encoderSz:    `(INLINE (EMBED () (EXTERNAL "abc") ""))`,
			encoderSHTML: `((img (@ (src . "abc"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Inline HTML Zettel",
		zmk:   `@@<hr>@@{="html"}`,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(INLINE)`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   ``,
		},
	},
	{
		descr: "Inline Text Zettel",
		zmk:   `@@<hr>@@{="text"}`,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "<hr>",
			encoderSz:    `(INLINE (LITERAL-ZETTEL (("" . "text")) "<hr>"))`,
			encoderSHTML: `(())`,
			encoderText:  `<hr>`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(INLINE)`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 (
	"fmt"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"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/szenc"    // Allow to use sz encoder.
	_ "zettelstore.de/z/encoder/textenc"  // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zmkenc"   // Allow to use zmk encoder.
	"zettelstore.de/z/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 (
	encoderHTML  = api.EncoderHTML
	encoderMD    = api.EncoderMD
	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
	}
	executeTestCases(t, append(tcsBlock, tcsInline...))
}

func executeTestCases(t *testing.T, testCases []zmkTestCase) {
	for testNum, tc := range testCases {
		inp := input.NewInput([]byte(tc.zmk))
		var pe parserEncoder
		if tc.inline {
			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)
		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, &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.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 checkSz(t *testing.T, testNum int, pe parserEncoder, descr string) {
	t.Helper()
	encdr := encoder.Create(encoderSz, nil)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxreader.MakeReader(strings.NewReader(exp)).Read()
	if err != nil {
		t.Error(err)
		return
	}
	got := val.String()
	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 sb strings.Builder
	if _, err := encdr.WriteInlines(&sb, &in.is); err != nil {
		return "", err
	}
	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 sb strings.Builder
	if _, err := encdr.WriteBlocks(&sb, &bl.bs); err != nil {
		return "", err
	}
	return sb.String(), nil

}
func (peBlocks) mode() string { return "block" }

Deleted encoder/env.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package encoder provides a generic interface to encode the abstract syntax
// tree into some text form.
package encoder

import "zettelstore.de/z/ast"

// Environment specifies all data and functions that affects encoding.
type Environment struct {
	// Important for many encoder.
	LinkAdapter  func(*ast.LinkNode) ast.InlineNode
	ImageAdapter func(*ast.ImageNode) ast.InlineNode
	CiteAdapter  func(*ast.CiteNode) ast.InlineNode

	// Important for HTML encoder
	Lang           string // default language
	Interactive    bool   // Encoded data will be placed in interactive content
	Xhtml          bool   // use XHTML syntax instead of HTML syntax
	MarkerExternal string // Marker after link to (external) material.
	NewWindow      bool   // open link in new window
	IgnoreMeta     map[string]bool
	footnotes      []*ast.FootnoteNode // Stores footnotes detected while encoding
}

// AdaptLink helps to call the link adapter.
func (env *Environment) AdaptLink(ln *ast.LinkNode) (*ast.LinkNode, ast.InlineNode) {
	if env == nil || env.LinkAdapter == nil {
		return ln, nil
	}
	n := env.LinkAdapter(ln)
	if n == nil {
		return ln, nil
	}
	if ln2, ok := n.(*ast.LinkNode); ok {
		return ln2, nil
	}
	return nil, n
}

// AdaptImage helps to call the link adapter.
func (env *Environment) AdaptImage(in *ast.ImageNode) (*ast.ImageNode, ast.InlineNode) {
	if env == nil || env.ImageAdapter == nil {
		return in, nil
	}
	n := env.ImageAdapter(in)
	if n == nil {
		return in, nil
	}
	if in2, ok := n.(*ast.ImageNode); ok {
		return in2, nil
	}
	return nil, n
}

// AdaptCite helps to call the link adapter.
func (env *Environment) AdaptCite(cn *ast.CiteNode) (*ast.CiteNode, ast.InlineNode) {
	if env == nil || env.CiteAdapter == nil {
		return cn, nil
	}
	n := env.CiteAdapter(cn)
	if n == nil {
		return cn, nil
	}
	if cn2, ok := n.(*ast.CiteNode); ok {
		return cn2, nil
	}
	return nil, n
}

// IsInteractive returns true, if Interactive is enabled and currently embedded
// interactive encoding will take place.
func (env *Environment) IsInteractive(inInteractive bool) bool {
	return inInteractive && env != nil && env.Interactive
}

// IsXHTML return true, if XHTML is enabled.
func (env *Environment) IsXHTML() bool {
	return env != nil && env.Xhtml
}

// HasNewWindow retruns true, if a new browser windows should be opened.
func (env *Environment) HasNewWindow() bool {
	return env != nil && env.NewWindow
}

// AddFootnote adds a footnote node to the environment and returns the number of that footnote.
func (env *Environment) AddFootnote(fn *ast.FootnoteNode) int {
	if env == nil {
		return 0
	}
	env.footnotes = append(env.footnotes, fn)
	return len(env.footnotes)
}

// GetCleanFootnotes returns the list of remembered footnote and forgets about them.
func (env *Environment) GetCleanFootnotes() []*ast.FootnoteNode {
	if env == nil {
		return nil
	}
	result := env.footnotes
	env.footnotes = nil
	return result
}

Deleted encoder/htmlenc/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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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










































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"fmt"
	"strconv"
	"strings"

	"zettelstore.de/z/ast"
)

// VisitPara emits HTML code for a paragraph: <p>...</p>
func (v *visitor) VisitPara(pn *ast.ParaNode) {
	v.b.WriteString("<p>")
	v.acceptInlineSlice(pn.Inlines)
	v.writeEndPara()
}

// VisitVerbatim emits HTML code for verbatim lines.
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	switch vn.Code {
	case ast.VerbatimProg:
		oldVisible := v.visibleSpace
		if vn.Attrs != nil {
			v.visibleSpace = vn.Attrs.HasDefault()
		}
		v.b.WriteString("<pre><code")
		v.visitAttributes(vn.Attrs)
		v.b.WriteByte('>')
		for _, line := range vn.Lines {
			v.writeHTMLEscaped(line)
			v.b.WriteByte('\n')
		}
		v.b.WriteString("</code></pre>\n")
		v.visibleSpace = oldVisible

	case ast.VerbatimComment:
		if vn.Attrs.HasDefault() {
			v.b.WriteString("<!--\n")
			for _, line := range vn.Lines {
				v.writeHTMLEscaped(line)
				v.b.WriteByte('\n')
			}
			v.b.WriteString("-->\n")
		}

	case ast.VerbatimHTML:
		for _, line := range vn.Lines {
			if !ignoreHTMLText(line) {
				v.b.WriteStrings(line, "\n")
			}
		}
	default:
		panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
	}
}

var htmlSnippetsIgnore = []string{
	"<script",
	"</script",
	"<iframe",
	"</iframe",
}

func ignoreHTMLText(s string) bool {
	lower := strings.ToLower(s)
	for _, snippet := range htmlSnippetsIgnore {
		if strings.Contains(lower, snippet) {
			return true
		}
	}
	return false
}

var specialSpanAttr = map[string]bool{
	"example":   true,
	"note":      true,
	"tip":       true,
	"important": true,
	"caution":   true,
	"warning":   true,
}

func processSpanAttributes(attrs *ast.Attributes) *ast.Attributes {
	if attrVal, ok := attrs.Get(""); ok {
		attrVal = strings.ToLower(attrVal)
		if specialSpanAttr[attrVal] {
			attrs = attrs.Clone()
			attrs.Remove("")
			attrs = attrs.AddClass("zs-indication").AddClass("zs-" + attrVal)
		}
	}
	return attrs
}

// VisitRegion writes HTML code for block regions.
func (v *visitor) VisitRegion(rn *ast.RegionNode) {
	var code string
	attrs := rn.Attrs
	oldVerse := v.inVerse
	switch rn.Code {
	case ast.RegionSpan:
		code = "div"
		attrs = processSpanAttributes(attrs)
	case ast.RegionVerse:
		v.inVerse = true
		code = "div"
	case ast.RegionQuote:
		code = "blockquote"
	default:
		panic(fmt.Sprintf("Unknown region code %v", rn.Code))
	}

	v.lang.push(attrs)
	defer v.lang.pop()

	v.b.WriteStrings("<", code)
	v.visitAttributes(attrs)
	v.b.WriteString(">\n")
	v.acceptBlockSlice(rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.b.WriteString("<cite>")
		v.acceptInlineSlice(rn.Inlines)
		v.b.WriteString("</cite>\n")
	}
	v.b.WriteStrings("</", code, ">\n")
	v.inVerse = oldVerse
}

// VisitHeading writes the HTML code for a heading.
func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
	v.lang.push(hn.Attrs)
	defer v.lang.pop()

	lvl := hn.Level
	if lvl > 6 {
		lvl = 6 // HTML has H1..H6
	}
	strLvl := strconv.Itoa(lvl)
	v.b.WriteStrings("<h", strLvl)
	v.visitAttributes(hn.Attrs)
	if slug := hn.Slug; len(slug) > 0 {
		v.b.WriteStrings(" id=\"", slug, "\"")
	}
	v.b.WriteByte('>')
	v.acceptInlineSlice(hn.Inlines)
	v.b.WriteStrings("</h", strLvl, ">\n")
}

// VisitHRule writes HTML code for a horizontal rule: <hr>.
func (v *visitor) VisitHRule(hn *ast.HRuleNode) {
	v.b.WriteString("<hr")
	v.visitAttributes(hn.Attrs)
	if v.env.IsXHTML() {
		v.b.WriteString(" />\n")
	} else {
		v.b.WriteString(">\n")
	}
}

var listCode = map[ast.NestedListCode]string{
	ast.NestedListOrdered:   "ol",
	ast.NestedListUnordered: "ul",
}

// VisitNestedList writes HTML code for lists and blockquotes.
func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
	v.lang.push(ln.Attrs)
	defer v.lang.pop()

	if ln.Code == ast.NestedListQuote {
		// NestedListQuote -> HTML <blockquote> doesn't use <li>...</li>
		v.writeQuotationList(ln)
		return
	}

	code, ok := listCode[ln.Code]
	if !ok {
		panic(fmt.Sprintf("Invalid list code %v", ln.Code))
	}

	compact := isCompactList(ln.Items)
	v.b.WriteStrings("<", code)
	v.visitAttributes(ln.Attrs)
	v.b.WriteString(">\n")
	for _, item := range ln.Items {
		v.b.WriteString("<li>")
		v.writeItemSliceOrPara(item, compact)
		v.b.WriteString("</li>\n")
	}
	v.b.WriteStrings("</", code, ">\n")
}

func (v *visitor) writeQuotationList(ln *ast.NestedListNode) {
	v.b.WriteString("<blockquote>\n")
	inPara := false
	for _, item := range ln.Items {
		if pn := getParaItem(item); pn != nil {
			if inPara {
				v.b.WriteByte('\n')
			} else {
				v.b.WriteString("<p>")
				inPara = true
			}
			v.acceptInlineSlice(pn.Inlines)
		} else {
			if inPara {
				v.writeEndPara()
				inPara = false
			}
			v.acceptItemSlice(item)
		}
	}
	if inPara {
		v.writeEndPara()
	}
	v.b.WriteString("</blockquote>\n")
}

func getParaItem(its ast.ItemSlice) *ast.ParaNode {
	if len(its) != 1 {
		return nil
	}
	if pn, ok := its[0].(*ast.ParaNode); ok {
		return pn
	}
	return nil
}

func isCompactList(insl []ast.ItemSlice) bool {
	for _, ins := range insl {
		if !isCompactSlice(ins) {
			return false
		}
	}
	return true
}

func isCompactSlice(ins ast.ItemSlice) bool {
	if len(ins) < 1 {
		return true
	}
	if len(ins) == 1 {
		switch ins[0].(type) {
		case *ast.ParaNode, *ast.VerbatimNode, *ast.HRuleNode:
			return true
		case *ast.NestedListNode:
			return false
		}
	}
	return false
}

// writeItemSliceOrPara emits the content of a paragraph if the paragraph is
// the only element of the block slice and if compact mode is true. Otherwise,
// the item slice is emitted normally.
func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) {
	if compact && len(ins) == 1 {
		if para, ok := ins[0].(*ast.ParaNode); ok {
			v.acceptInlineSlice(para.Inlines)
			return
		}
	}
	v.acceptItemSlice(ins)
}

func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) {
	if len(ds) == 1 {
		if para, ok := ds[0].(*ast.ParaNode); ok {
			v.acceptInlineSlice(para.Inlines)
			return
		}
	}
	for _, dn := range ds {
		dn.Accept(v)
	}
}

// VisitDescriptionList emits a HTML description list.
func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	v.b.WriteString("<dl>\n")
	for _, descr := range dn.Descriptions {
		v.b.WriteString("<dt>")
		v.acceptInlineSlice(descr.Term)
		v.b.WriteString("</dt>\n")

		for _, b := range descr.Descriptions {
			v.b.WriteString("<dd>")
			v.writeDescriptionsSlice(b)
			v.b.WriteString("</dd>\n")
		}
	}
	v.b.WriteString("</dl>\n")
}

// VisitTable emits a HTML table.
func (v *visitor) VisitTable(tn *ast.TableNode) {
	v.b.WriteString("<table>\n")
	if len(tn.Header) > 0 {
		v.b.WriteString("<thead>\n")
		v.writeRow(tn.Header, "<th", "</th>")
		v.b.WriteString("</thead>\n")
	}
	if len(tn.Rows) > 0 {
		v.b.WriteString("<tbody>\n")
		for _, row := range tn.Rows {
			v.writeRow(row, "<td", "</td>")
		}
		v.b.WriteString("</tbody>\n")
	}
	v.b.WriteString("</table>\n")
}

var alignStyle = map[ast.Alignment]string{
	ast.AlignDefault: ">",
	ast.AlignLeft:    " style=\"text-align:left\">",
	ast.AlignCenter:  " style=\"text-align:center\">",
	ast.AlignRight:   " style=\"text-align:right\">",
}

func (v *visitor) writeRow(row ast.TableRow, cellStart, cellEnd string) {
	v.b.WriteString("<tr>")
	for _, cell := range row {
		v.b.WriteString(cellStart)
		if len(cell.Inlines) == 0 {
			v.b.WriteByte('>')
		} else {
			v.b.WriteString(alignStyle[cell.Align])
			v.acceptInlineSlice(cell.Inlines)
		}
		v.b.WriteString(cellEnd)
	}
	v.b.WriteString("</tr>\n")
}

// VisitBLOB writes the binary object as a value.
func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {
	switch bn.Syntax {
	case "gif", "jpeg", "png":
		v.b.WriteStrings("<img src=\"data:image/", bn.Syntax, ";base64,")
		v.b.WriteBase64(bn.Blob)
		v.b.WriteString("\" title=\"")
		v.writeQuotedEscaped(bn.Title)
		v.b.WriteString("\">\n")
	default:
		v.b.WriteStrings("<p class=\"error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>\n")
	}
}

func (v *visitor) writeEndPara() {
	v.b.WriteString("</p>\n")
}

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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25



26
27
28
29
30
31
32
33







34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56



57
58
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

-
+

-
+




+
+
+


-
+






+
+
+
+

-
-
-
+
+
+

+



-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
+
+
+
+
-
+

-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
-
-
-
+
+
+
-
+

-
+

+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
+
+
+
-
-
+
-
-
+
-
-
-
-

-
+
-
-
-
-
+


-
-
+
+



-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5.
// Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client.
package htmlenc

import (
	"io"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/encfun"
	"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("html", encoder.Info{
		Create: func(env *encoder.Environment) encoder.Encoder { return &htmlEncoder{env: env} },
	})
}

type htmlEncoder struct {
	env *encoder.Environment
	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.
	return &Encoder{
		tx:      szenc.NewTransformer(),
		th:      shtml.NewEvaluator(1),
		lang:    params.Lang,
		textEnc: textenc.Create(),
	}
}

type Encoder struct {
	tx      *szenc.Transformer
	th      *shtml.Evaluator
	lang    string
	textEnc *textenc.Encoder
}

// WriteZettel encodes a full zettel as HTML5.
func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newVisitor(he, w)
	if !he.env.IsXHTML() {
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 {
		v.b.WriteString("<!DOCTYPE html>\n")
		return 0, err
	}
	if env := he.env; env != nil && env.Lang == "" {

		v.b.WriteStrings("<html>\n<head>")
	} else {
		v.b.WriteStrings("<html lang=\"", env.Lang, "\">")
	var isTitle ast.InlineSlice
	var htitle *sx.Pair
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		isTitle = parser.ParseSpacedText(plainTitle)
		xtitle := he.tx.GetSz(&isTitle)
		htitle, err = he.th.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 := he.th.Endnotes(&env)

	var head sx.ListBuilder
	head.Add(shtml.SymHead)
	head.Add(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.String("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta))
	v.b.WriteString("\n<head>\n<meta charset=\"utf-8\">\n")
	v.b.WriteStrings("<title>", encfun.MetaAsText(zn.InhMeta, meta.KeyTitle), "</title>")
	if inhMeta {
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
		v.acceptMeta(zn.InhMeta)
		he.textEnc.WriteInlines(&sb, &isTitle)
	} else {
		v.acceptMeta(zn.Meta)
		sb.Write(zn.Meta.Zid.Bytes())
	}
	head.Add(sx.MakeList(shtml.SymAttrTitle, sx.String(sb.String())))
	v.b.WriteString("\n</head>\n<body>\n")
	v.acceptBlockSlice(zn.Ast)
	v.writeEndnotes()
	v.b.WriteString("</body>\n</html>")
	length, err := v.b.Flush()
	return length, err

	var body sx.ListBuilder
	body.Add(shtml.SymBody)
	if hasTitle {
		body.Add(htitle.Cons(shtml.SymH1))
	}
	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()),
	)

	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	v := newVisitor(he, w)

func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env)
	// Write title
	if title, ok := m.Get(meta.KeyTitle); ok {
	if err != nil {
		textEnc := encoder.Create("text", nil)
		var sb strings.Builder
		return 0, err
		textEnc.WriteInlines(&sb, parser.ParseMetadata(title))
		v.b.WriteStrings("<meta name=\"zs-", meta.KeyTitle, "\" content=\"")
		v.writeQuotedEscaped(sb.String())
		v.b.WriteString("\">")
	}

	gen := sxhtml.NewGenerator().SetNewline()
	// Write other metadata
	v.acceptMeta(m)
	length, err := v.b.Flush()
	return length, err
	return gen.WriteListHTML(w, hm)
}

func (he *htmlEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, zn.Ast)
func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks encodes a block slice.
func (he *htmlEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
	v := newVisitor(he, w)
	v.acceptBlockSlice(bs)
	v.writeEndnotes()
	length, err := v.b.Flush()
	return length, err
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()
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, he.th.Endnotes(&env))
		length += l
		return length, err2
	}
	return 0, err
}

// WriteInlines writes an inline slice to the writer
func (he *htmlEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
	v := newVisitor(he, w)
	if env := he.env; env != nil {
func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(is), &env)
	if err == nil {
		gen := sxhtml.NewGenerator()
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
		v.inInteractive = env.Interactive
	}
			return length, err2
		}
	v.acceptInlineSlice(is)
	length, err := v.b.Flush()
	return length, err
		return length, nil
	}
	return 0, err
}

Deleted encoder/htmlenc/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
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




































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"fmt"
	"strconv"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
)

// VisitText writes text content.
func (v *visitor) VisitText(tn *ast.TextNode) {
	v.writeHTMLEscaped(tn.Text)
}

// VisitTag writes tag content.
func (v *visitor) VisitTag(tn *ast.TagNode) {
	// TODO: erst mal als span. Link wäre gut, muss man vermutlich via Callback lösen.
	v.b.WriteString("<span class=\"zettel-tag\">#")
	v.writeHTMLEscaped(tn.Tag)
	v.b.WriteString("</span>")
}

// VisitSpace emits a white space.
func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
	if v.inVerse || v.env.IsXHTML() {
		v.b.WriteString(sn.Lexeme)
	} else {
		v.b.WriteByte(' ')
	}
}

// VisitBreak writes HTML code for line breaks.
func (v *visitor) VisitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		if v.env.IsXHTML() {
			v.b.WriteString("<br />\n")
		} else {
			v.b.WriteString("<br>\n")
		}
	} else {
		v.b.WriteByte('\n')
	}
}

// VisitLink writes HTML code for links.
func (v *visitor) VisitLink(ln *ast.LinkNode) {
	ln, n := v.env.AdaptLink(ln)
	if n != nil {
		n.Accept(v)
		return
	}
	v.lang.push(ln.Attrs)
	defer v.lang.pop()

	switch ln.Ref.State {
	case ast.RefStateSelf, ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased:
		v.writeAHref(ln.Ref, ln.Attrs, ln.Inlines)
	case ast.RefStateBroken:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "zs-broken")
		attrs = attrs.Set("title", "Zettel not found") // l10n
		v.writeAHref(ln.Ref, attrs, ln.Inlines)
	case ast.RefStateExternal:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "zs-external")
		if v.env.HasNewWindow() {
			attrs = attrs.Set("target", "_blank").Set("rel", "noopener noreferrer")
		}
		v.writeAHref(ln.Ref, attrs, ln.Inlines)
		if v.env != nil {
			v.b.WriteString(v.env.MarkerExternal)
		}
	default:
		if v.env.IsInteractive(v.inInteractive) {
			v.writeSpan(ln.Inlines, ln.Attrs)
			return
		}
		v.b.WriteString("<a href=\"")
		v.writeQuotedEscaped(ln.Ref.Value)
		v.b.WriteByte('"')
		v.visitAttributes(ln.Attrs)
		v.b.WriteByte('>')
		v.inInteractive = true
		v.acceptInlineSlice(ln.Inlines)
		v.inInteractive = false
		v.b.WriteString("</a>")
	}
}

func (v *visitor) writeAHref(ref *ast.Reference, attrs *ast.Attributes, ins ast.InlineSlice) {
	if v.env.IsInteractive(v.inInteractive) {
		v.writeSpan(ins, attrs)
		return
	}
	v.b.WriteString("<a href=\"")
	v.writeReference(ref)
	v.b.WriteByte('"')
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	v.inInteractive = true
	v.acceptInlineSlice(ins)
	v.inInteractive = false
	v.b.WriteString("</a>")
}

// VisitImage writes HTML code for images.
func (v *visitor) VisitImage(in *ast.ImageNode) {
	in, n := v.env.AdaptImage(in)
	if n != nil {
		n.Accept(v)
		return
	}
	v.lang.push(in.Attrs)
	defer v.lang.pop()

	if in.Ref == nil {
		v.b.WriteString("<img src=\"data:image/")
		switch in.Syntax {
		case "svg":
			v.b.WriteString("svg+xml;utf8,")
			v.writeQuotedEscaped(string(in.Blob))
		default:
			v.b.WriteStrings(in.Syntax, ";base64,")
			v.b.WriteBase64(in.Blob)
		}
	} else {
		v.b.WriteString("<img src=\"")
		v.writeReference(in.Ref)
	}
	v.b.WriteString("\" alt=\"")
	v.acceptInlineSlice(in.Inlines)
	v.b.WriteByte('"')
	v.visitAttributes(in.Attrs)
	if v.env.IsXHTML() {
		v.b.WriteString(" />")
	} else {
		v.b.WriteByte('>')
	}
}

// VisitCite writes code for citations.
func (v *visitor) VisitCite(cn *ast.CiteNode) {
	cn, n := v.env.AdaptCite(cn)
	if n != nil {
		n.Accept(v)
		return
	}
	if cn == nil {
		return
	}
	v.lang.push(cn.Attrs)
	defer v.lang.pop()
	v.b.WriteString(cn.Key)
	if len(cn.Inlines) > 0 {
		v.b.WriteString(", ")
		v.acceptInlineSlice(cn.Inlines)
	}
}

// VisitFootnote write HTML code for a footnote.
func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) {
	v.lang.push(fn.Attrs)
	defer v.lang.pop()
	if v.env.IsInteractive(v.inInteractive) {
		return
	}

	n := strconv.Itoa(v.env.AddFootnote(fn))
	v.b.WriteStrings("<sup id=\"fnref:", n, "\"><a href=\"#fn:", n, "\" class=\"zs-footnote-ref\" role=\"doc-noteref\">", n, "</a></sup>")
	// TODO: what to do with Attrs?
}

// VisitMark writes HTML code to mark a position.
func (v *visitor) VisitMark(mn *ast.MarkNode) {
	if v.env.IsInteractive(v.inInteractive) {
		return
	}
	if len(mn.Text) > 0 {
		v.b.WriteStrings("<a id=\"", mn.Text, "\"></a>")
	}
}

// VisitFormat write HTML code for formatting text.
func (v *visitor) VisitFormat(fn *ast.FormatNode) {
	v.lang.push(fn.Attrs)
	defer v.lang.pop()

	var code string
	attrs := fn.Attrs
	switch fn.Code {
	case ast.FormatItalic:
		code = "i"
	case ast.FormatEmph:
		code = "em"
	case ast.FormatBold:
		code = "b"
	case ast.FormatStrong:
		code = "strong"
	case ast.FormatUnder:
		code = "u" // TODO: ändern in <span class="XXX">
	case ast.FormatInsert:
		code = "ins"
	case ast.FormatStrike:
		code = "s"
	case ast.FormatDelete:
		code = "del"
	case ast.FormatSuper:
		code = "sup"
	case ast.FormatSub:
		code = "sub"
	case ast.FormatQuotation:
		code = "q"
	case ast.FormatSmall:
		code = "small"
	case ast.FormatSpan:
		v.writeSpan(fn.Inlines, processSpanAttributes(attrs))
		return
	case ast.FormatMonospace:
		code = "span"
		attrs = attrs.Set("style", "font-family:monospace")
	case ast.FormatQuote:
		v.visitQuotes(fn)
		return
	default:
		panic(fmt.Sprintf("Unknown format code %v", fn.Code))
	}
	v.b.WriteStrings("<", code)
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	v.acceptInlineSlice(fn.Inlines)
	v.b.WriteStrings("</", code, ">")
}

func (v *visitor) writeSpan(ins ast.InlineSlice, attrs *ast.Attributes) {
	v.b.WriteString("<span")
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	v.acceptInlineSlice(ins)
	v.b.WriteString("</span>")

}

var langQuotes = map[string][2]string{
	meta.ValueLangEN: {"&ldquo;", "&rdquo;"},
	"de":             {"&bdquo;", "&ldquo;"},
	"fr":             {"&laquo;&nbsp;", "&nbsp;&raquo;"},
}

func getQuotes(lang string) (string, string) {
	langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' })
	for len(langFields) > 0 {
		langSup := strings.Join(langFields, "-")
		quotes, ok := langQuotes[langSup]
		if ok {
			return quotes[0], quotes[1]
		}
		langFields = langFields[0 : len(langFields)-1]
	}
	return "\"", "\""
}

func (v *visitor) visitQuotes(fn *ast.FormatNode) {
	_, withSpan := fn.Attrs.Get("lang")
	if withSpan {
		v.b.WriteString("<span")
		v.visitAttributes(fn.Attrs)
		v.b.WriteByte('>')
	}
	openingQ, closingQ := getQuotes(v.lang.top())
	v.b.WriteString(openingQ)
	v.acceptInlineSlice(fn.Inlines)
	v.b.WriteString(closingQ)
	if withSpan {
		v.b.WriteString("</span>")
	}
}

// VisitLiteral write HTML code for literal inline text.
func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
	switch ln.Code {
	case ast.LiteralProg:
		v.writeLiteral("<code", "</code>", ln.Attrs, ln.Text)
	case ast.LiteralKeyb:
		v.writeLiteral("<kbd", "</kbd>", ln.Attrs, ln.Text)
	case ast.LiteralOutput:
		v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text)
	case ast.LiteralComment:
		v.b.WriteString("<!-- ")
		v.writeHTMLEscaped(ln.Text) // writeCommentEscaped
		v.b.WriteString(" -->")
	case ast.LiteralHTML:
		if !ignoreHTMLText(ln.Text) {
			v.b.WriteString(ln.Text)
		}
	default:
		panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
	}
}

func (v *visitor) writeLiteral(codeS, codeE string, attrs *ast.Attributes, text string) {
	oldVisible := v.visibleSpace
	if attrs != nil {
		v.visibleSpace = attrs.HasDefault()
	}
	v.b.WriteString(codeS)
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	v.writeHTMLEscaped(text)
	v.b.WriteString(codeE)
	v.visibleSpace = oldVisible
}

Deleted encoder/htmlenc/langstack.go.

1
2
3
4
5
6
7
8
9
10
11
12
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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import "zettelstore.de/z/ast"

type langStack struct {
	items []string
}

func newLangStack(lang string) langStack {
	items := make([]string, 1, 16)
	items[0] = lang
	return langStack{items}
}

func (s langStack) top() string { return s.items[len(s.items)-1] }

func (s *langStack) pop() { s.items = s.items[0 : len(s.items)-1] }

func (s *langStack) push(attrs *ast.Attributes) {
	if value, ok := attrs.Get("lang"); ok {
		s.items = append(s.items, value)
	} else {
		s.items = append(s.items, s.top())
	}
}

Deleted encoder/htmlenc/langstack_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) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"testing"

	"zettelstore.de/z/ast"
)

func TestStackSimple(t *testing.T) {
	exp := "de"
	s := newLangStack(exp)
	if got := s.top(); got != exp {
		t.Errorf("Init: expected %q, but got %q", exp, got)
		return
	}

	a := &ast.Attributes{}
	s.push(a)
	if got := s.top(); exp != got {
		t.Errorf("Empty push: expected %q, but got %q", exp, got)
	}

	exp2 := "en"
	a = a.Set("lang", exp2)
	s.push(a)
	if got := s.top(); exp2 != got {
		t.Errorf("Full push: expected %q, but got %q", exp2, got)
	}

	s.pop()
	if got := s.top(); exp != got {
		t.Errorf("pop: expected %q, but got %q", exp, got)
	}
}

Deleted encoder/htmlenc/visitor.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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





































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"io"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/strfun"
)

// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
	env           *encoder.Environment
	b             encoder.BufWriter
	visibleSpace  bool // Show space character in raw text
	inVerse       bool // In verse block
	inInteractive bool // Rendered interactive HTML code
	lang          langStack
}

func newVisitor(he *htmlEncoder, w io.Writer) *visitor {
	var lang string
	if he.env != nil {
		lang = he.env.Lang
	}
	return &visitor{
		env:  he.env,
		b:    encoder.NewBufWriter(w),
		lang: newLangStack(lang),
	}
}

var mapMetaKey = map[string]string{
	meta.KeyCopyright: "copyright",
	meta.KeyLicense:   "license",
}

func (v *visitor) acceptMeta(m *meta.Meta) {
	for _, pair := range m.Pairs(true) {
		if env := v.env; env != nil && env.IgnoreMeta[pair.Key] {
			continue
		}
		if pair.Key == meta.KeyTitle {
			continue
		}
		if pair.Key == meta.KeyTags {
			v.writeTags(pair.Value)
		} else if key, ok := mapMetaKey[pair.Key]; ok {
			v.writeMeta("", key, pair.Value)
		} else {
			v.writeMeta("zs-", pair.Key, pair.Value)
		}
	}
}

func (v *visitor) writeTags(tags string) {
	v.b.WriteString("\n<meta name=\"keywords\" content=\"")
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			v.b.WriteString(", ")
		}
		v.writeQuotedEscaped(strings.TrimPrefix(val, "#"))
	}
	v.b.WriteString("\">")
}

func (v *visitor) writeMeta(prefix, key, value string) {
	v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"")
	v.writeQuotedEscaped(value)
	v.b.WriteString("\">")
}

func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
	for _, bn := range bns {
		bn.Accept(v)
	}
}
func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
	for _, in := range ins {
		in.Accept(v)
	}
}
func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
	for _, in := range ins {
		in.Accept(v)
	}
}

func (v *visitor) writeEndnotes() {
	footnotes := v.env.GetCleanFootnotes()
	if len(footnotes) > 0 {
		v.b.WriteString("<ol class=\"zs-endnotes\">\n")
		for i := 0; i < len(footnotes); i++ {
			// Do not use a range loop above, because a footnote may contain
			// a footnote. Therefore v.enc.footnote may grow during the loop.
			fn := footnotes[i]
			n := strconv.Itoa(i + 1)
			v.b.WriteStrings("<li id=\"fn:", n, "\" role=\"doc-endnote\">")
			v.acceptInlineSlice(fn.Inlines)
			v.b.WriteStrings(
				" <a href=\"#fnref:",
				n,
				"\" class=\"zs-footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></li>\n")
		}
		v.b.WriteString("</ol>\n")
	}
}

// visitAttributes write HTML attributes
func (v *visitor) visitAttributes(a *ast.Attributes) {
	if a == nil || len(a.Attrs) == 0 {
		return
	}
	keys := make([]string, 0, len(a.Attrs))
	for k := range a.Attrs {
		if k != "-" {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)

	for _, k := range keys {
		if k == "" || k == "-" {
			continue
		}
		v.b.WriteStrings(" ", k)
		vl := a.Attrs[k]
		if len(vl) > 0 {
			v.b.WriteString("=\"")
			v.writeQuotedEscaped(vl)
			v.b.WriteByte('"')
		}
	}
}

func (v *visitor) writeHTMLEscaped(s string) {
	strfun.HTMLEscape(&v.b, s, v.visibleSpace)
}

func (v *visitor) writeQuotedEscaped(s string) {
	strfun.HTMLAttrEscape(&v.b, s)
}

func (v *visitor) writeReference(ref *ast.Reference) {
	if ref.URL == nil {
		v.writeHTMLEscaped(ref.Value)
		return
	}
	v.b.WriteString(ref.URL.String())
}

Deleted encoder/jsonenc/djsonenc.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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






























































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package jsonenc encodes the abstract syntax tree into JSON.
package jsonenc

import (
	"fmt"
	"io"
	"sort"
	"strconv"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/encfun"
)

func init() {
	encoder.Register("djson", encoder.Info{
		Create: func(env *encoder.Environment) encoder.Encoder { return &jsonDetailEncoder{env: env} },
	})
}

type jsonDetailEncoder struct {
	env *encoder.Environment
}

// WriteZettel writes the encoded zettel to the writer.
func (je *jsonDetailEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newDetailVisitor(w, je)
	v.b.WriteString("{\"meta\":{\"title\":")
	v.acceptInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle))
	if inhMeta {
		v.writeMeta(zn.InhMeta)
	} else {
		v.writeMeta(zn.Meta)
	}
	v.b.WriteByte('}')
	v.b.WriteString(",\"content\":")
	v.acceptBlockSlice(zn.Ast)
	v.b.WriteByte('}')
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as JSON.
func (je *jsonDetailEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	v := newDetailVisitor(w, je)
	v.b.WriteString("{\"title\":")
	v.acceptInlineSlice(encfun.MetaAsInlineSlice(m, meta.KeyTitle))
	v.writeMeta(m)
	v.b.WriteByte('}')
	length, err := v.b.Flush()
	return length, err
}

func (je *jsonDetailEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return je.WriteBlocks(w, zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (je *jsonDetailEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
	v := newDetailVisitor(w, je)
	v.acceptBlockSlice(bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (je *jsonDetailEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
	v := newDetailVisitor(w, je)
	v.acceptInlineSlice(is)
	length, err := v.b.Flush()
	return length, err
}

// detailVisitor writes the abstract syntax tree to an io.Writer.
type detailVisitor struct {
	b   encoder.BufWriter
	env *encoder.Environment
}

func newDetailVisitor(w io.Writer, je *jsonDetailEncoder) *detailVisitor {
	return &detailVisitor{b: encoder.NewBufWriter(w), env: je.env}
}

// VisitPara emits JSON code for a paragraph.
func (v *detailVisitor) VisitPara(pn *ast.ParaNode) {
	v.writeNodeStart("Para")
	v.writeContentStart('i')
	v.acceptInlineSlice(pn.Inlines)
	v.b.WriteByte('}')
}

var verbatimCode = map[ast.VerbatimCode]string{
	ast.VerbatimProg:    "CodeBlock",
	ast.VerbatimComment: "CommentBlock",
	ast.VerbatimHTML:    "HTMLBlock",
}

// VisitVerbatim emits JSON code for verbatim lines.
func (v *detailVisitor) VisitVerbatim(vn *ast.VerbatimNode) {
	code, ok := verbatimCode[vn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
	}
	v.writeNodeStart(code)
	v.visitAttributes(vn.Attrs)
	v.writeContentStart('l')
	for i, line := range vn.Lines {
		if i > 0 {
			v.b.WriteByte(',')
		}
		writeEscaped(&v.b, line)
	}
	v.b.WriteString("]}")
}

var regionCode = map[ast.RegionCode]string{
	ast.RegionSpan:  "SpanBlock",
	ast.RegionQuote: "QuoteBlock",
	ast.RegionVerse: "VerseBlock",
}

// VisitRegion writes JSON code for block regions.
func (v *detailVisitor) VisitRegion(rn *ast.RegionNode) {
	code, ok := regionCode[rn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown region code %v", rn.Code))
	}
	v.writeNodeStart(code)
	v.visitAttributes(rn.Attrs)
	v.writeContentStart('b')
	v.acceptBlockSlice(rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.writeContentStart('i')
		v.acceptInlineSlice(rn.Inlines)
	}
	v.b.WriteByte('}')
}

// VisitHeading writes the JSON code for a heading.
func (v *detailVisitor) VisitHeading(hn *ast.HeadingNode) {
	v.writeNodeStart("Heading")
	v.visitAttributes(hn.Attrs)
	v.writeContentStart('n')
	v.b.WriteString(strconv.Itoa(hn.Level))
	if slug := hn.Slug; len(slug) > 0 {
		v.writeContentStart('s')
		v.b.WriteStrings("\"", slug, "\"")
	}
	v.writeContentStart('i')
	v.acceptInlineSlice(hn.Inlines)
	v.b.WriteByte('}')
}

// VisitHRule writes JSON code for a horizontal rule: <hr>.
func (v *detailVisitor) VisitHRule(hn *ast.HRuleNode) {
	v.writeNodeStart("Hrule")
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte('}')
}

var listCode = map[ast.NestedListCode]string{
	ast.NestedListOrdered:   "OrderedList",
	ast.NestedListUnordered: "BulletList",
	ast.NestedListQuote:     "QuoteList",
}

// VisitNestedList writes JSON code for lists and blockquotes.
func (v *detailVisitor) VisitNestedList(ln *ast.NestedListNode) {
	v.writeNodeStart(listCode[ln.Code])
	v.writeContentStart('c')
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.acceptItemSlice(item)
	}
	v.b.WriteString("]}")
}

// VisitDescriptionList emits a JSON description list.
func (v *detailVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	v.writeNodeStart("DescriptionList")
	v.writeContentStart('g')
	for i, def := range dn.Descriptions {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.b.WriteByte('[')
		v.acceptInlineSlice(def.Term)

		if len(def.Descriptions) > 0 {
			for _, b := range def.Descriptions {
				v.b.WriteByte(',')
				v.acceptDescriptionSlice(b)
			}
		}
		v.b.WriteByte(']')
	}
	v.b.WriteString("]}")
}

// VisitTable emits a JSON table.
func (v *detailVisitor) VisitTable(tn *ast.TableNode) {
	v.writeNodeStart("Table")
	v.writeContentStart('p')

	// Table header
	v.b.WriteByte('[')
	for i, cell := range tn.Header {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.writeCell(cell)
	}
	v.b.WriteString("],")

	// Table rows
	v.b.WriteByte('[')
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.b.WriteByte('[')
		for j, cell := range row {
			if j > 0 {
				v.b.WriteByte(',')
			}
			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 *detailVisitor) writeCell(cell *ast.TableCell) {
	v.b.WriteString(alignmentCode[cell.Align])
	v.acceptInlineSlice(cell.Inlines)
	v.b.WriteByte(']')
}

// VisitBLOB writes the binary object as a value.
func (v *detailVisitor) VisitBLOB(bn *ast.BLOBNode) {
	v.writeNodeStart("Blob")
	v.writeContentStart('q')
	writeEscaped(&v.b, bn.Title)
	v.writeContentStart('s')
	writeEscaped(&v.b, bn.Syntax)
	v.writeContentStart('o')
	v.b.WriteBase64(bn.Blob)
	v.b.WriteString("\"}")
}

// VisitText writes text content.
func (v *detailVisitor) VisitText(tn *ast.TextNode) {
	v.writeNodeStart("Text")
	v.writeContentStart('s')
	writeEscaped(&v.b, tn.Text)
	v.b.WriteByte('}')
}

// VisitTag writes tag content.
func (v *detailVisitor) VisitTag(tn *ast.TagNode) {
	v.writeNodeStart("Tag")
	v.writeContentStart('s')
	writeEscaped(&v.b, tn.Tag)
	v.b.WriteByte('}')
}

// VisitSpace emits a white space.
func (v *detailVisitor) VisitSpace(sn *ast.SpaceNode) {
	v.writeNodeStart("Space")
	if l := len(sn.Lexeme); l > 1 {
		v.writeContentStart('n')
		v.b.WriteString(strconv.Itoa(l))
	}
	v.b.WriteByte('}')
}

// VisitBreak writes JSON code for line breaks.
func (v *detailVisitor) VisitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.writeNodeStart("Hard")
	} else {
		v.writeNodeStart("Soft")
	}
	v.b.WriteByte('}')
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:  "invalid",
	ast.RefStateZettel:   "zettel",
	ast.RefStateSelf:     "self",
	ast.RefStateFound:    "zettel",
	ast.RefStateBroken:   "broken",
	ast.RefStateHosted:   "local",
	ast.RefStateBased:    "based",
	ast.RefStateExternal: "external",
}

// VisitLink writes JSON code for links.
func (v *detailVisitor) VisitLink(ln *ast.LinkNode) {
	ln, n := v.env.AdaptLink(ln)
	if n != nil {
		n.Accept(v)
		return
	}
	v.writeNodeStart("Link")
	v.visitAttributes(ln.Attrs)
	v.writeContentStart('q')
	writeEscaped(&v.b, mapRefState[ln.Ref.State])
	v.writeContentStart('s')
	writeEscaped(&v.b, ln.Ref.String())
	v.writeContentStart('i')
	v.acceptInlineSlice(ln.Inlines)
	v.b.WriteByte('}')
}

// VisitImage writes JSON code for images.
func (v *detailVisitor) VisitImage(in *ast.ImageNode) {
	in, n := v.env.AdaptImage(in)
	if n != nil {
		n.Accept(v)
		return
	}
	v.writeNodeStart("Image")
	v.visitAttributes(in.Attrs)
	if in.Ref == nil {
		v.writeContentStart('j')
		v.b.WriteString("\"s\":")
		writeEscaped(&v.b, in.Syntax)
		switch in.Syntax {
		case "svg":
			v.writeContentStart('q')
			writeEscaped(&v.b, string(in.Blob))
		default:
			v.writeContentStart('o')
			v.b.WriteBase64(in.Blob)
			v.b.WriteByte('"')
		}
		v.b.WriteByte('}')
	} else {
		v.writeContentStart('s')
		writeEscaped(&v.b, in.Ref.String())
	}
	if len(in.Inlines) > 0 {
		v.writeContentStart('i')
		v.acceptInlineSlice(in.Inlines)
	}
	v.b.WriteByte('}')
}

// VisitCite writes code for citations.
func (v *detailVisitor) VisitCite(cn *ast.CiteNode) {
	v.writeNodeStart("Cite")
	v.visitAttributes(cn.Attrs)
	v.writeContentStart('s')
	writeEscaped(&v.b, cn.Key)
	if len(cn.Inlines) > 0 {
		v.writeContentStart('i')
		v.acceptInlineSlice(cn.Inlines)
	}
	v.b.WriteByte('}')
}

// VisitFootnote write JSON code for a footnote.
func (v *detailVisitor) VisitFootnote(fn *ast.FootnoteNode) {
	v.writeNodeStart("Footnote")
	v.visitAttributes(fn.Attrs)
	v.writeContentStart('i')
	v.acceptInlineSlice(fn.Inlines)
	v.b.WriteByte('}')
}

// VisitMark writes JSON code to mark a position.
func (v *detailVisitor) VisitMark(mn *ast.MarkNode) {
	v.writeNodeStart("Mark")
	if len(mn.Text) > 0 {
		v.writeContentStart('s')
		writeEscaped(&v.b, mn.Text)
	}
	v.b.WriteByte('}')
}

var formatCode = map[ast.FormatCode]string{
	ast.FormatItalic:    "Italic",
	ast.FormatEmph:      "Emph",
	ast.FormatBold:      "Bold",
	ast.FormatStrong:    "Strong",
	ast.FormatMonospace: "Mono",
	ast.FormatStrike:    "Strikethrough",
	ast.FormatDelete:    "Delete",
	ast.FormatUnder:     "Underline",
	ast.FormatInsert:    "Insert",
	ast.FormatSuper:     "Super",
	ast.FormatSub:       "Sub",
	ast.FormatQuote:     "Quote",
	ast.FormatQuotation: "Quotation",
	ast.FormatSmall:     "Small",
	ast.FormatSpan:      "Span",
}

// VisitFormat write JSON code for formatting text.
func (v *detailVisitor) VisitFormat(fn *ast.FormatNode) {
	v.writeNodeStart(formatCode[fn.Code])
	v.visitAttributes(fn.Attrs)
	v.writeContentStart('i')
	v.acceptInlineSlice(fn.Inlines)
	v.b.WriteByte('}')
}

var literalCode = map[ast.LiteralCode]string{
	ast.LiteralProg:    "Code",
	ast.LiteralKeyb:    "Input",
	ast.LiteralOutput:  "Output",
	ast.LiteralComment: "Comment",
	ast.LiteralHTML:    "HTML",
}

// VisitLiteral write JSON code for literal inline text.
func (v *detailVisitor) VisitLiteral(ln *ast.LiteralNode) {
	code, ok := literalCode[ln.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
	}
	v.writeNodeStart(code)
	v.visitAttributes(ln.Attrs)
	v.writeContentStart('s')
	writeEscaped(&v.b, ln.Text)
	v.b.WriteByte('}')
}

func (v *detailVisitor) acceptBlockSlice(bns ast.BlockSlice) {
	v.b.WriteByte('[')
	for i, bn := range bns {
		if i > 0 {
			v.b.WriteByte(',')
		}
		bn.Accept(v)
	}
	v.b.WriteByte(']')
}

func (v *detailVisitor) acceptItemSlice(ins ast.ItemSlice) {
	v.b.WriteByte('[')
	for i, in := range ins {
		if i > 0 {
			v.b.WriteByte(',')
		}
		in.Accept(v)
	}
	v.b.WriteByte(']')
}

func (v *detailVisitor) acceptDescriptionSlice(dns ast.DescriptionSlice) {
	v.b.WriteByte('[')
	for i, dn := range dns {
		if i > 0 {
			v.b.WriteByte(',')
		}
		dn.Accept(v)
	}
	v.b.WriteByte(']')
}

func (v *detailVisitor) acceptInlineSlice(ins ast.InlineSlice) {
	v.b.WriteByte('[')
	for i, in := range ins {
		if i > 0 {
			v.b.WriteByte(',')
		}
		in.Accept(v)
	}
	v.b.WriteByte(']')
}

// visitAttributes write JSON attributes
func (v *detailVisitor) visitAttributes(a *ast.Attributes) {
	if a == nil || len(a.Attrs) == 0 {
		return
	}
	keys := make([]string, 0, len(a.Attrs))
	for k := range a.Attrs {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	v.b.WriteString(",\"a\":{\"")
	for i, k := range keys {
		if i > 0 {
			v.b.WriteString("\",\"")
		}
		v.b.Write(Escape(k))
		v.b.WriteString("\":\"")
		v.b.Write(Escape(a.Attrs[k]))
	}
	v.b.WriteString("\"}")
}

func (v *detailVisitor) writeNodeStart(t string) {
	v.b.WriteStrings("{\"t\":\"", t, "\"")
}

var contentCode = map[rune][]byte{
	'b': []byte(",\"b\":"),   // List of blocks
	'c': []byte(",\"c\":["),  // List of list of blocks
	'g': []byte(",\"g\":["),  // General list
	'i': []byte(",\"i\":"),   // List of inlines
	'j': []byte(",\"j\":{"),  // Embedded JSON object
	'l': []byte(",\"l\":["),  // List of lines
	'n': []byte(",\"n\":"),   // Number
	'o': []byte(",\"o\":\""), // Byte object
	'p': []byte(",\"p\":["),  // Generic tuple
	'q': []byte(",\"q\":"),   // String, if 's' is also needed
	's': []byte(",\"s\":"),   // String
	't': []byte("Content code 't' is not allowed"),
	'y': []byte("Content code 'y' is not allowed"), // field after 'j'
}

func (v *detailVisitor) writeContentStart(code rune) {
	if b, ok := contentCode[code]; ok {
		v.b.Write(b)
		return
	}
	panic("Unknown content code " + strconv.Itoa(int(code)))
}

func (v *detailVisitor) writeMeta(m *meta.Meta) {
	for _, p := range m.Pairs(true) {
		if p.Key == meta.KeyTitle {
			continue
		}
		v.b.WriteString(",\"")
		v.b.Write(Escape(p.Key))
		v.b.WriteString("\":")
		if m.Type(p.Key).IsSet {
			v.writeSetValue(p.Value)
		} else {
			v.b.WriteByte('"')
			v.b.Write(Escape(p.Value))
			v.b.WriteByte('"')
		}
	}
}

func (v *detailVisitor) writeSetValue(value string) {
	v.b.WriteByte('[')
	for i, val := range meta.ListFromValue(value) {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.b.WriteByte('"')
		v.b.Write(Escape(val))
		v.b.WriteByte('"')
	}
	v.b.WriteByte(']')
}

Deleted encoder/jsonenc/jsonenc.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package jsonenc encodes the abstract syntax tree into some JSON formats.
package jsonenc

import (
	"bytes"
	"io"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register("json", encoder.Info{
		Create:  func(*encoder.Environment) encoder.Encoder { return &jsonEncoder{} },
		Default: true,
	})
}

// jsonEncoder is just a stub. It is not implemented. The real implementation
// is in file web/adapter/json.go
type jsonEncoder struct{}

// WriteZettel writes the encoded zettel to the writer.
func (je *jsonEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	return 0, encoder.ErrNoWriteZettel
}

// WriteMeta encodes meta data as HTML5.
func (je *jsonEncoder) WriteMeta(w io.Writer, meta *meta.Meta) (int, error) {
	return 0, encoder.ErrNoWriteMeta
}

func (je *jsonEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return 0, encoder.ErrNoWriteContent
}

// WriteBlocks writes a block slice to the writer
func (je *jsonEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
	return 0, encoder.ErrNoWriteBlocks
}

// WriteInlines writes an inline slice to the writer
func (je *jsonEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
	return 0, encoder.ErrNoWriteInlines
}

var (
	jsBackslash   = []byte{'\\', '\\'}
	jsDoubleQuote = []byte{'\\', '"'}
	jsNewline     = []byte{'\\', 'n'}
	jsTab         = []byte{'\\', 't'}
	jsCr          = []byte{'\\', 'r'}
	jsUnicode     = []byte{'\\', 'u', '0', '0', '0', '0'}
	jsHex         = []byte("0123456789ABCDEF")
)

// Escape returns the given string as a byte slice, where every non-printable
// rune is made printable.
func Escape(s string) []byte {
	var buf bytes.Buffer

	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
			}
		}
		buf.WriteString(s[last:i])
		buf.Write(b)
		last = i + 1
	}
	buf.WriteString(s[last:])
	return buf.Bytes()
}

func writeEscaped(b *encoder.BufWriter, s string) {
	b.WriteByte('"')
	b.Write(Escape(s))
	b.WriteByte('"')
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderMD, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myME }

type Encoder struct{}

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)
	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)
	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')
	}
}

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)
	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)
	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
}

func newVisitor(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)
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.b.WriteString("---")
	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)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.FootnoteNode:
		return nil // Should write no content
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteString("\n\n")
		}
		ast.Walk(v, bn)
	}
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	lc := len(vn.Content)
	if vn.Kind != ast.VerbatimProg || lc == 0 {
		return
	}
	v.writeSpaces(4)
	lcm1 := lc - 1
	for i := 0; i < lc; i++ {
		b := vn.Content[i]
		if b != '\n' && b != '\r' {
			v.b.WriteByte(b)
			continue
		}
		j := i + 1
		for ; j < lc; j++ {
			c := vn.Content[j]
			if c != '\n' && c != '\r' {
				break
			}
		}
		if j >= lcm1 {
			break
		}
		v.b.WriteByte('\n')
		v.writeSpaces(4)
		i = j - 1
	}
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	if rn.Kind != ast.RegionQuote {
		return
	}
	first := true
	for _, bn := range rn.Blocks {
		pn, ok := bn.(*ast.ParaNode)
		if !ok {
			continue
		}
		if !first {
			v.b.WriteString("\n\n")
		}
		first = false
		v.b.WriteString("> ")
		ast.Walk(v, &pn.Inlines)
	}
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	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 {
	case ast.NestedListOrdered:
		v.writeNestedList(ln, "1. ")
	case ast.NestedListUnordered:
		v.writeNestedList(ln, "* ")
	case ast.NestedListQuote:
		v.writeListQuote(ln)
	}
	v.listInfo = v.listInfo[:len(v.listInfo)-1]
}

func (v *visitor) writeNestedList(ln *ast.NestedListNode, enum string) {
	v.listInfo = append(v.listInfo, len(enum))
	regIndent := 4*len(v.listInfo) - 4
	paraIndent := regIndent + len(enum)
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.writeSpaces(regIndent)
		v.b.WriteString(enum)
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				if _, ok := in.(*ast.ParaNode); ok {
					v.writeSpaces(paraIndent)
				}
			}
			ast.Walk(v, in)
		}
	}
}

func (v *visitor) writeListQuote(ln *ast.NestedListNode) {
	v.listInfo = append(v.listInfo, 0)
	if len(v.listInfo) > 1 {
		return
	}

	prefix := v.listPrefix
	v.listPrefix = "> "

	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.b.WriteString(v.listPrefix)
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				if _, ok := in.(*ast.ParaNode); ok {
					v.b.WriteString(v.listPrefix)
				}
			}
			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.writeReference(ln.Ref, ln.Inlines)
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	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)
	} else if len(is) > 0 {
		v.b.WriteByte('[')
		ast.Walk(v, &is)
		v.b.WriteStrings("](", ref.String())
		v.b.WriteByte(')')
	} else if isAutoLinkable(ref) {
		v.b.WriteByte('<')
		v.b.WriteString(ref.String())
		v.b.WriteByte('>')
	} else {
		s := ref.String()
		v.b.WriteStrings("[", s, "](", s, ")")
	}
}

func isAutoLinkable(ref *ast.Reference) bool {
	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	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.b.WriteString("<q>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</q>")
	case ast.FormatMark:
		v.b.WriteString("<mark>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</mark>")
	default:
		ast.Walk(v, &fn.Inlines)
	}
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralProg:
		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 range n {
		v.b.WriteByte(' ')
	}
}

Deleted encoder/nativeenc/nativeenc.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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






































































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package nativeenc encodes the abstract syntax tree into native format.
package nativeenc

import (
	"fmt"
	"io"
	"sort"
	"strconv"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/encfun"
	"zettelstore.de/z/parser"
)

func init() {
	encoder.Register("native", encoder.Info{
		Create: func(env *encoder.Environment) encoder.Encoder { return &nativeEncoder{env: env} },
	})
}

type nativeEncoder struct {
	env *encoder.Environment
}

// WriteZettel encodes the zettel to the writer.
func (ne *nativeEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newVisitor(w, ne)
	v.b.WriteString("[Title ")
	v.acceptInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle))
	v.b.WriteByte(']')
	if inhMeta {
		v.acceptMeta(zn.InhMeta, false)
	} else {
		v.acceptMeta(zn.Meta, false)
	}
	v.b.WriteByte('\n')
	v.acceptBlockSlice(zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data in native format.
func (ne *nativeEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	v := newVisitor(w, ne)
	v.acceptMeta(m, true)
	length, err := v.b.Flush()
	return length, err
}

func (ne *nativeEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ne.WriteBlocks(w, zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (ne *nativeEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
	v := newVisitor(w, ne)
	v.acceptBlockSlice(bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (ne *nativeEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
	v := newVisitor(w, ne)
	v.acceptInlineSlice(is)
	length, err := v.b.Flush()
	return length, err
}

// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
	b     encoder.BufWriter
	level int
	env   *encoder.Environment
}

func newVisitor(w io.Writer, enc *nativeEncoder) *visitor {
	return &visitor{b: encoder.NewBufWriter(w), env: enc.env}
}

var (
	rawBackslash   = []byte{'\\', '\\'}
	rawDoubleQuote = []byte{'\\', '"'}
	rawNewline     = []byte{'\\', 'n'}
)

func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) {
	if withTitle {
		v.b.WriteString("[Title ")
		v.acceptInlineSlice(parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")))
		v.b.WriteByte(']')
	}
	v.writeMetaString(m, meta.KeyRole, "Role")
	v.writeMetaList(m, meta.KeyTags, "Tags")
	v.writeMetaString(m, meta.KeySyntax, "Syntax")
	pairs := m.PairsRest(true)
	if len(pairs) == 0 {
		return
	}
	v.b.WriteString("\n[Header")
	v.level++
	for i, p := range pairs {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.b.WriteByte('[')
		v.b.WriteStrings(p.Key, " \"")
		v.writeEscaped(p.Value)
		v.b.WriteString("\"]")
	}
	v.level--
	v.b.WriteByte(']')
}

func (v *visitor) writeMetaString(m *meta.Meta, key, native string) {
	if val, ok := m.Get(key); ok && len(val) > 0 {
		v.b.WriteStrings("\n[", native, " \"", val, "\"]")
	}
}

func (v *visitor) writeMetaList(m *meta.Meta, key, native string) {
	if vals, ok := m.GetList(key); ok && len(vals) > 0 {
		v.b.WriteStrings("\n[", native)
		for _, val := range vals {
			v.b.WriteByte(' ')
			v.b.WriteString(val)
		}
		v.b.WriteByte(']')
	}
}

// VisitPara emits native code for a paragraph.
func (v *visitor) VisitPara(pn *ast.ParaNode) {
	v.b.WriteString("[Para ")
	v.acceptInlineSlice(pn.Inlines)
	v.b.WriteByte(']')
}

var verbatimCode = map[ast.VerbatimCode][]byte{
	ast.VerbatimProg:    []byte("[CodeBlock"),
	ast.VerbatimComment: []byte("[CommentBlock"),
	ast.VerbatimHTML:    []byte("[HTMLBlock"),
}

// VisitVerbatim emits native code for verbatim lines.
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	code, ok := verbatimCode[vn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
	}
	v.b.Write(code)
	v.visitAttributes(vn.Attrs)
	v.b.WriteString(" \"")
	for i, line := range vn.Lines {
		if i > 0 {
			v.b.Write(rawNewline)
		}
		v.writeEscaped(line)
	}
	v.b.WriteString("\"]")
}

var regionCode = map[ast.RegionCode][]byte{
	ast.RegionSpan:  []byte("[SpanBlock"),
	ast.RegionQuote: []byte("[QuoteBlock"),
	ast.RegionVerse: []byte("[VerseBlock"),
}

// VisitRegion writes native code for block regions.
func (v *visitor) VisitRegion(rn *ast.RegionNode) {
	code, ok := regionCode[rn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown region code %v", rn.Code))
	}
	v.b.Write(code)
	v.visitAttributes(rn.Attrs)
	v.level++
	v.writeNewLine()
	v.b.WriteByte('[')
	v.level++
	v.acceptBlockSlice(rn.Blocks)
	v.level--
	v.b.WriteByte(']')
	if len(rn.Inlines) > 0 {
		v.b.WriteByte(',')
		v.writeNewLine()
		v.b.WriteString("[Cite ")
		v.acceptInlineSlice(rn.Inlines)
		v.b.WriteByte(']')
	}
	v.level--
	v.b.WriteByte(']')
}

// VisitHeading writes the native code for a heading.
func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
	v.b.WriteStrings("[Heading ", strconv.Itoa(hn.Level), " \"", hn.Slug, "\"")
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte(' ')
	v.acceptInlineSlice(hn.Inlines)
	v.b.WriteByte(']')
}

// VisitHRule writes native code for a horizontal rule: <hr>.
func (v *visitor) VisitHRule(hn *ast.HRuleNode) {
	v.b.WriteString("[Hrule")
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte(']')
}

var listCode = map[ast.NestedListCode][]byte{
	ast.NestedListOrdered:   []byte("[OrderedList"),
	ast.NestedListUnordered: []byte("[BulletList"),
	ast.NestedListQuote:     []byte("[QuoteList"),
}

// VisitNestedList writes native code for lists and blockquotes.
func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
	v.b.Write(listCode[ln.Code])
	v.level++
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.level++
		v.b.WriteByte('[')
		v.acceptItemSlice(item)
		v.b.WriteByte(']')
		v.level--
	}
	v.level--
	v.b.WriteByte(']')
}

// VisitDescriptionList emits a native description list.
func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	v.b.WriteString("[DescriptionList")
	v.level++
	for i, descr := range dn.Descriptions {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.b.WriteString("[Term [")
		v.acceptInlineSlice(descr.Term)
		v.b.WriteByte(']')

		if len(descr.Descriptions) > 0 {
			v.level++
			for _, b := range descr.Descriptions {
				v.b.WriteByte(',')
				v.writeNewLine()
				v.b.WriteString("[Description")
				v.level++
				v.writeNewLine()
				v.acceptDescriptionSlice(b)
				v.b.WriteByte(']')
				v.level--
			}
			v.level--
		}
		v.b.WriteByte(']')
	}
	v.level--
	v.b.WriteByte(']')
}

// VisitTable emits a native table.
func (v *visitor) VisitTable(tn *ast.TableNode) {
	v.b.WriteString("[Table")
	v.level++
	if len(tn.Header) > 0 {
		v.writeNewLine()
		v.b.WriteString("[Header ")
		for i, cell := range tn.Header {
			if i > 0 {
				v.b.WriteByte(',')
			}
			v.writeCell(cell)
		}
		v.b.WriteString("],")
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.b.WriteString("[Row ")
		for j, cell := range row {
			if j > 0 {
				v.b.WriteByte(',')
			}
			v.writeCell(cell)
		}
		v.b.WriteByte(']')
	}
	v.level--
	v.b.WriteByte(']')
}

var alignString = map[ast.Alignment]string{
	ast.AlignDefault: " Default",
	ast.AlignLeft:    " Left",
	ast.AlignCenter:  " Center",
	ast.AlignRight:   " Right",
}

func (v *visitor) writeCell(cell *ast.TableCell) {
	v.b.WriteStrings("[Cell", alignString[cell.Align])
	if len(cell.Inlines) > 0 {
		v.b.WriteByte(' ')
		v.acceptInlineSlice(cell.Inlines)
	}
	v.b.WriteByte(']')
}

// VisitBLOB writes the binary object as a value.
func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {
	v.b.WriteString("[BLOB \"")
	v.writeEscaped(bn.Title)
	v.b.WriteString("\" \"")
	v.writeEscaped(bn.Syntax)
	v.b.WriteString("\" \"")
	v.b.WriteBase64(bn.Blob)
	v.b.WriteString("\"]")
}

// VisitText writes text content.
func (v *visitor) VisitText(tn *ast.TextNode) {
	v.b.WriteString("Text \"")
	v.writeEscaped(tn.Text)
	v.b.WriteByte('"')
}

// VisitTag writes tag content.
func (v *visitor) VisitTag(tn *ast.TagNode) {
	v.b.WriteString("Tag \"")
	v.writeEscaped(tn.Tag)
	v.b.WriteByte('"')
}

// VisitSpace emits a white space.
func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
	v.b.WriteString("Space")
	if l := len(sn.Lexeme); l > 1 {
		v.b.WriteByte(' ')
		v.b.WriteString(strconv.Itoa(l))
	}
}

// VisitBreak writes native code for line breaks.
func (v *visitor) VisitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("Break")
	} else {
		v.b.WriteString("Space")
	}
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:  "INVALID",
	ast.RefStateZettel:   "ZETTEL",
	ast.RefStateSelf:     "SELF",
	ast.RefStateFound:    "ZETTEL",
	ast.RefStateBroken:   "BROKEN",
	ast.RefStateHosted:   "LOCAL",
	ast.RefStateBased:    "BASED",
	ast.RefStateExternal: "EXTERNAL",
}

// VisitLink writes native code for links.
func (v *visitor) VisitLink(ln *ast.LinkNode) {
	ln, n := v.env.AdaptLink(ln)
	if n != nil {
		n.Accept(v)
		return
	}
	v.b.WriteString("Link")
	v.visitAttributes(ln.Attrs)
	v.b.WriteByte(' ')
	v.b.WriteString(mapRefState[ln.Ref.State])
	v.b.WriteString(" \"")
	v.writeEscaped(ln.Ref.String())
	v.b.WriteString("\" [")
	if !ln.OnlyRef {
		v.acceptInlineSlice(ln.Inlines)
	}
	v.b.WriteByte(']')
}

// VisitImage writes native code for images.
func (v *visitor) VisitImage(in *ast.ImageNode) {
	in, n := v.env.AdaptImage(in)
	if n != nil {
		n.Accept(v)
		return
	}
	v.b.WriteString("Image")
	v.visitAttributes(in.Attrs)
	if in.Ref == nil {
		v.b.WriteStrings(" {\"", in.Syntax, "\" \"")
		switch in.Syntax {
		case "svg":
			v.writeEscaped(string(in.Blob))
		default:
			v.b.WriteString("\" \"")
			v.b.WriteBase64(in.Blob)
		}
		v.b.WriteString("\"}")
	} else {
		v.b.WriteStrings(" \"", in.Ref.String(), "\"")
	}
	if len(in.Inlines) > 0 {
		v.b.WriteString(" [")
		v.acceptInlineSlice(in.Inlines)
		v.b.WriteByte(']')
	}
}

// VisitCite writes code for citations.
func (v *visitor) VisitCite(cn *ast.CiteNode) {
	v.b.WriteString("Cite")
	v.visitAttributes(cn.Attrs)
	v.b.WriteString(" \"")
	v.writeEscaped(cn.Key)
	v.b.WriteByte('"')
	if len(cn.Inlines) > 0 {
		v.b.WriteString(" [")
		v.acceptInlineSlice(cn.Inlines)
		v.b.WriteByte(']')
	}
}

// VisitFootnote write native code for a footnote.
func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) {
	v.b.WriteString("Footnote")
	v.visitAttributes(fn.Attrs)
	v.b.WriteString(" [")
	v.acceptInlineSlice(fn.Inlines)
	v.b.WriteByte(']')
}

// VisitMark writes native code to mark a position.
func (v *visitor) VisitMark(mn *ast.MarkNode) {
	v.b.WriteString("Mark")
	if len(mn.Text) > 0 {
		v.b.WriteString(" \"")
		v.writeEscaped(mn.Text)
		v.b.WriteByte('"')
	}
}

var formatCode = map[ast.FormatCode][]byte{
	ast.FormatItalic:    []byte("Italic"),
	ast.FormatEmph:      []byte("Emph"),
	ast.FormatBold:      []byte("Bold"),
	ast.FormatStrong:    []byte("Strong"),
	ast.FormatUnder:     []byte("Underline"),
	ast.FormatInsert:    []byte("Insert"),
	ast.FormatMonospace: []byte("Mono"),
	ast.FormatStrike:    []byte("Strikethrough"),
	ast.FormatDelete:    []byte("Delete"),
	ast.FormatSuper:     []byte("Super"),
	ast.FormatSub:       []byte("Sub"),
	ast.FormatQuote:     []byte("Quote"),
	ast.FormatQuotation: []byte("Quotation"),
	ast.FormatSmall:     []byte("Small"),
	ast.FormatSpan:      []byte("Span"),
}

// VisitFormat write native code for formatting text.
func (v *visitor) VisitFormat(fn *ast.FormatNode) {
	v.b.Write(formatCode[fn.Code])
	v.visitAttributes(fn.Attrs)
	v.b.WriteString(" [")
	v.acceptInlineSlice(fn.Inlines)
	v.b.WriteByte(']')
}

var literalCode = map[ast.LiteralCode][]byte{
	ast.LiteralProg:    []byte("Code"),
	ast.LiteralKeyb:    []byte("Input"),
	ast.LiteralOutput:  []byte("Output"),
	ast.LiteralComment: []byte("Comment"),
	ast.LiteralHTML:    []byte("HTML"),
}

// VisitLiteral write native code for code inline text.
func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
	code, ok := literalCode[ln.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
	}
	v.b.Write(code)
	v.visitAttributes(ln.Attrs)
	v.b.WriteString(" \"")
	v.writeEscaped(ln.Text)
	v.b.WriteByte('"')
}

func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
	for i, bn := range bns {
		if i > 0 {
			v.b.WriteByte(',')
			v.writeNewLine()
		}
		bn.Accept(v)
	}
}
func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
	for i, in := range ins {
		if i > 0 {
			v.b.WriteByte(',')
			v.writeNewLine()
		}
		in.Accept(v)
	}
}
func (v *visitor) acceptDescriptionSlice(dns ast.DescriptionSlice) {
	for i, dn := range dns {
		if i > 0 {
			v.b.WriteByte(',')
			v.writeNewLine()
		}
		dn.Accept(v)
	}
}
func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
	for i, in := range ins {
		if i > 0 {
			v.b.WriteByte(',')
		}
		in.Accept(v)
	}
}

// visitAttributes write native attributes
func (v *visitor) visitAttributes(a *ast.Attributes) {
	if a == nil || len(a.Attrs) == 0 {
		return
	}
	keys := make([]string, 0, len(a.Attrs))
	for k := range a.Attrs {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	v.b.WriteString(" (\"")
	if val, ok := a.Attrs[""]; ok {
		v.writeEscaped(val)
	}
	v.b.WriteString("\",[")
	first := true
	for _, k := range keys {
		if k == "" {
			continue
		}
		if !first {
			v.b.WriteByte(',')
		}
		v.b.WriteString(k)
		val := a.Attrs[k]
		if len(val) > 0 {
			v.b.WriteString("=\"")
			v.writeEscaped(val)
			v.b.WriteByte('"')
		}
		first = false
	}
	v.b.WriteString("])")
}

func (v *visitor) writeNewLine() {
	v.b.WriteByte('\n')
	for i := 0; i < v.level; i++ {
		v.b.WriteByte(' ')
	}
}

func (v *visitor) writeEscaped(s string) {
	last := 0
	for i, ch := range s {
		var b []byte
		switch ch {
		case '\n':
			b = rawNewline
		case '"':
			b = rawDoubleQuote
		case '\\':
			b = rawBackslash
		default:
			continue
		}
		v.b.WriteString(s[last:i])
		v.b.Write(b)
		last = i + 1
	}
	v.b.WriteString(s[last:])
}

Deleted encoder/rawenc/rawenc.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package rawenc encodes the abstract syntax tree as raw content.
package rawenc

import (
	"io"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register("raw", encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &rawEncoder{} },
	})
}

type rawEncoder struct{}

// WriteZettel writes the encoded zettel to the writer.
func (re *rawEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	b := encoder.NewBufWriter(w)
	if inhMeta {
		zn.InhMeta.Write(&b, true)
	} else {
		zn.Meta.Write(&b, true)
	}
	b.WriteByte('\n')
	b.WriteString(zn.Content.AsString())
	length, err := b.Flush()
	return length, err
}

// WriteMeta encodes meta data as HTML5.
func (re *rawEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	b := encoder.NewBufWriter(w)
	m.Write(&b, true)
	length, err := b.Flush()
	return length, err
}

func (re *rawEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	b := encoder.NewBufWriter(w)
	b.WriteString(zn.Content.AsString())
	length, err := b.Flush()
	return length, err
}

// WriteBlocks writes a block slice to the writer
func (re *rawEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
	return 0, encoder.ErrNoWriteBlocks
}

// WriteInlines writes an inline slice to the writer
func (re *rawEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
	return 0, encoder.ErrNoWriteInlines
}

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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	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,
	}
}

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)
}

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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSz, func(*encoder.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()}
}

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)
}

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
412
413
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/sz"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	t := Transformer{}
	return &t
}

type Transformer struct {
	inVerse bool
}

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.String(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.String(n.Fragment)).
			Cons(sx.String(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.String(n.Text))
	case *ast.SpaceNode:
		if t.inVerse {
			return sx.MakeList(sz.SymSpace, sx.String(n.Lexeme))
		}
		return sx.MakeList(sz.SymSpace)
	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.String(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.String(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.String(n.Fragment)).
			Cons(sx.String(n.Slug)).
			Cons(sx.String(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.String(string(n.Content)),
		)
	}
	return sx.MakeList(sz.SymUnknown, sx.String(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.String(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sx.MakeList(
		sz.SymBLOB,
		t.getInlineList(bn.Description),
		sx.String(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.String(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.String(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return tail.Cons(sx.String(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.String(k), sx.String(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.String(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,
}

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.String(val)
			}
			obj = sx.MakeList(setObjs...)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			obj = t.getInlineList(is)
		} else {
			obj = sx.String(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.String(sb.String())
	}
	return sx.String("")
}

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
37
38
39

40
41
42
43
44
45
46
47



48
49
50
51
52
53
54
55
56

57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


20
21
22

23
24
25
26


27

28
29
30
31
32

33
34
35
36
37

38
39


40




41
42
43
44
45
46



47
48
49
50






51

52





53
54

55
56

57
58

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

-
+

-
+




+
+
+








-
-
+
+

-
+



-
-
+
-


+
+
+
-
+

+
+

-
+

-
-
+
-
-
-
-
+





-
-
-
+
+
+

-
-
-
-
-
-

-
+
-
-
-
-
-

+
-
+

-
+

-
+

-
+



+
+
+
+
+
+
+
+
+
+
-
-
+
+



-
+

-
+





-
+

-
+






-
+
+



-
+


-
-
-
+
+
+
+
+
+
+
-
-
+
-
-
-
-
+
+
+
-
-
-
+
+
+

-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
+
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+

-
-
-
-
-
+
-
-
-
-
-
+

+
+
-
-
+
+
-
-
-

-
-
-
+
-
-
+
-
-
-
+
-
-
-
+

+
-
+
-
-
-
-
+
+

-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
-
-


-
-
+
-
-
+
+
+
-
-
-
-
+


-
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
+
-
-
-
-
+
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+


+
-
-
+
+
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+


//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package textenc encodes the abstract syntax tree into its text.
package textenc

import (
	"io"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register("text", encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &textEncoder{} },
	encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
	})
}

// Create an encoder.
func Create() *Encoder { return &myTE }

type textEncoder struct{}
type Encoder struct{}

var myTE Encoder // Only a singleton is required.

// WriteZettel writes metadata and content.
func (te *textEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	if inhMeta {
		te.WriteMeta(&v.b, zn.InhMeta)
	te.WriteMeta(&v.b, zn.InhMeta, evalMeta)
	} else {
		te.WriteMeta(&v.b, zn.Meta)
	}
	v.acceptBlockSlice(zn.Ast)
	v.visitBlockSlice(&zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes metadata as text.
func (te *textEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	b := encoder.NewBufWriter(w)
	for _, pair := range m.Pairs(true) {
func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	buf := encoder.NewEncWriter(w)
	for _, pair := range m.ComputedPairs() {
		switch meta.Type(pair.Key) {
		case meta.TypeBool:
			if meta.BoolValue(pair.Value) {
				b.WriteString("true")
			} else {
				b.WriteString("false")
			}
		case meta.TypeTagSet:
			for i, tag := range meta.ListFromValue(pair.Value) {
			writeTagSet(&buf, meta.ListFromValue(pair.Value))
				if i > 0 {
					b.WriteByte(' ')
				}
				b.WriteString(meta.CleanTag(tag))
			}
		case meta.TypeZettelmarkup:
			is := evalMeta(pair.Value)
			te.WriteInlines(w, parser.ParseMetadata(pair.Value))
			te.WriteInlines(&buf, &is)
		default:
			b.WriteString(pair.Value)
			buf.WriteString(pair.Value)
		}
		b.WriteByte('\n')
		buf.WriteByte('\n')
	}
	length, err := b.Flush()
	length, err := buf.Flush()
	return length, err
}

func writeTagSet(buf *encoder.EncWriter, tags []string) {
	for i, tag := range tags {
		if i > 0 {
			buf.WriteByte(' ')
		}
		buf.WriteString(meta.CleanTag(tag))
	}

}

func (te *textEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return te.WriteBlocks(w, zn.Ast)
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 (te *textEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
	v.acceptBlockSlice(bs)
	v.visitBlockSlice(bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (te *textEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w)
	v.acceptInlineSlice(is)
	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.BufWriter
	b         encoder.EncWriter
	inlinePos int
}

func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewBufWriter(w)}
	return &visitor{b: encoder.NewEncWriter(w)}
}

// VisitPara emits text code for a paragraph
func (v *visitor) VisitPara(pn *ast.ParaNode) {
	v.acceptInlineSlice(pn.Inlines)
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.visitInlineSlice(n)
}

		return nil
// VisitVerbatim emits text for verbatim lines.
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	if vn.Code == ast.VerbatimComment {
		return
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
		return nil
	}
	for i, line := range vn.Lines {
		if i > 0 {
	case *ast.RegionNode:
		v.visitBlockSlice(&n.Blocks)
		if len(n.Inlines) > 0 {
			v.b.WriteByte('\n')
		}
		v.b.WriteString(line)
			ast.Walk(v, &n.Inlines)
	}
}

// VisitRegion writes text code for block regions.
func (v *visitor) VisitRegion(rn *ast.RegionNode) {
	v.acceptBlockSlice(rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.b.WriteByte('\n')
		v.acceptInlineSlice(rn.Inlines)
		}
		return nil
	case *ast.NestedListNode:
		v.visitNestedList(n)
		return nil
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
		return nil
	}
}

// VisitHeading writes the text code for a heading.
func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
	case *ast.TableNode:
		v.visitTable(n)
		return nil
	case *ast.TranscludeNode, *ast.BLOBNode:
	v.acceptInlineSlice(hn.Inlines)
}

		return nil
// VisitHRule writes nothing for a horizontal rule.
func (v *visitor) VisitHRule(hn *ast.HRuleNode) {}

	case *ast.TextNode:
		v.b.WriteString(n.Text)
// VisitNestedList writes text code for lists and blockquotes.
func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		if i > 0 {
		return nil
	case *ast.SpaceNode:
		v.b.WriteByte(' ')
		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteByte('\n')
		}
		v.acceptItemSlice(item)
	}
}

		} else {
// VisitDescriptionList emits a text for a description list.
func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	for i, descr := range dn.Descriptions {
		if i > 0 {
			v.b.WriteByte('\n')
			v.b.WriteByte(' ')
		}
		return nil
	case *ast.LinkNode:
		v.acceptInlineSlice(descr.Term)

		if len(n.Inlines) > 0 {
			ast.Walk(v, &n.Inlines)
		for _, b := range descr.Descriptions {
			v.b.WriteByte('\n')
			v.acceptDescriptionSlice(b)
		}
	}
}

		return nil
// VisitTable emits a text table.
func (v *visitor) VisitTable(tn *ast.TableNode) {
	case *ast.MarkNode:
	if len(tn.Header) > 0 {
		for i, cell := range tn.Header {
			if i > 0 {
		if len(n.Inlines) > 0 {
				v.b.WriteByte(' ')
			}
			v.acceptInlineSlice(cell.Inlines)
			ast.Walk(v, &n.Inlines)
		}
		return nil
		v.b.WriteByte('\n')
	case *ast.FootnoteNode:
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte('\n')
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
		}
		for j, cell := range row {
			if j > 0 {
				v.b.WriteByte(' ')
			}
		// No 'return nil' to write text
	case *ast.LiteralNode:
		if n.Kind != ast.LiteralComment {
			v.b.Write(n.Content)
		}
			v.acceptInlineSlice(cell.Inlines)
		}
	}
}

// VisitBLOB writes nothing, because it contains no text.
func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {}

// VisitText writes text content.
	}
	return v
func (v *visitor) VisitText(tn *ast.TextNode) {
	v.b.WriteString(tn.Text)
}

// VisitTag writes tag content.
func (v *visitor) VisitTag(tn *ast.TagNode) {
func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	v.b.WriteStrings("#", tn.Tag)
}
	if vn.Kind == ast.VerbatimComment {
		return
	}

// VisitSpace emits a white space.
func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
	v.b.WriteByte(' ')
	v.b.Write(vn.Content)
}

// VisitBreak writes text code for line breaks.
func (v *visitor) VisitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteByte('\n')
	} else {
		v.b.WriteByte(' ')
func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		v.writePosChar(i, '\n')
		for j, it := range item {
			v.writePosChar(j, '\n')
	}
}

			ast.Walk(v, it)
		}
	}
// VisitLink writes text code for links.
func (v *visitor) VisitLink(ln *ast.LinkNode) {
	if !ln.OnlyRef {
		v.acceptInlineSlice(ln.Inlines)
	}
}

// VisitImage writes text code for images.
func (v *visitor) VisitImage(in *ast.ImageNode) {
	v.acceptInlineSlice(in.Inlines)
}

}

func (v *visitor) visitDescriptionList(dl *ast.DescriptionListNode) {
	for i, descr := range dl.Descriptions {
		v.writePosChar(i, '\n')
		ast.Walk(v, &descr.Term)
		for _, b := range descr.Descriptions {
			v.b.WriteByte('\n')
			for k, d := range b {
				v.writePosChar(k, '\n')
				ast.Walk(v, d)
			}
		}
// VisitCite writes code for citations.
func (v *visitor) VisitCite(cn *ast.CiteNode) {
	v.acceptInlineSlice(cn.Inlines)
}

	}
}
// VisitFootnote write text code for a footnote.
func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) {
	v.b.WriteByte(' ')
	v.acceptInlineSlice(fn.Inlines)
}


// VisitMark writes nothing for a mark.
func (v *visitor) VisitMark(mn *ast.MarkNode) {}

func (v *visitor) visitTable(tn *ast.TableNode) {
	if len(tn.Header) > 0 {
// VisitFormat write text code for formatting text.
func (v *visitor) VisitFormat(fn *ast.FormatNode) {
	v.acceptInlineSlice(fn.Inlines)
}

		v.writeRow(tn.Header)
// VisitLiteral write text code for literal inline text.
func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
	if ln.Code != ast.LiteralComment {
		v.b.WriteString(ln.Text)
	}
}

// VisitAttributes never writes any attribute data.
func (v *visitor) VisitAttributes(a *ast.Attributes) {}

func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
	for i, bn := range bns {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		bn.Accept(v)
		v.b.WriteByte('\n')
	}
	for i, row := range tn.Rows {
		v.writePosChar(i, '\n')
		v.writeRow(row)
	}
}

func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
	for i, in := range ins {
func (v *visitor) writeRow(row ast.TableRow) {
	for i, cell := range row {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.writePosChar(i, ' ')
		ast.Walk(v, &cell.Inlines)
	}
		in.Accept(v)
	}
}
func (v *visitor) acceptDescriptionSlice(dns ast.DescriptionSlice) {
	for i, dn := range dns {
}

func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.writePosChar(i, '\n')
		ast.Walk(v, bn)
	}
		dn.Accept(v)
	}
}
func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
	for _, in := range ins {
		in.Accept(v)
}

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) writePosChar(pos int, ch byte) {
	if pos > 0 {
		v.b.WriteByte(ch)
	}
}

Added encoder/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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
)

// EncWriter is a specialized writer for encoding zettel.
type EncWriter struct {
	w      io.Writer // The io.Writer to write to
	err    error     // Collect error
	length int       // Collected length
}

// NewEncWriter creates a new EncWriter
func NewEncWriter(w io.Writer) EncWriter {
	return EncWriter{w: w}
}

// Write writes the content of p.
func (w *EncWriter) Write(p []byte) (l int, err error) {
	if w.err != nil {
		return 0, w.err
	}
	l, w.err = w.w.Write(p)
	w.length += l
	return l, w.err
}

// WriteString writes the content of s.
func (w *EncWriter) WriteString(s string) {
	if w.err != nil {
		return
	}
	var l int
	l, w.err = io.WriteString(w.w, s)
	w.length += l
}

// WriteStrings writes the content of sl.
func (w *EncWriter) WriteStrings(sl ...string) {
	for _, s := range sl {
		w.WriteString(s)
	}
}

// WriteByte writes the content of b.
func (w *EncWriter) WriteByte(b byte) error {
	var l int
	l, w.err = w.Write([]byte{b})
	w.length += l
	return w.err
}

// WriteBytes writes the content of bs.
func (w *EncWriter) WriteBytes(bs ...byte) {
	w.Write(bs)
}

// WriteBase64 writes the content of p, encoded with base64.
func (w *EncWriter) WriteBase64(p []byte) {
	if w.err == nil {
		encoder := base64.NewEncoder(base64.StdEncoding, w.w)
		var l int
		l, w.err = encoder.Write(p)
		w.length += l
		err1 := encoder.Close()
		if w.err == nil {
			w.err = err1
		}
	}
}

// Flush returns the collected length and error.
func (w *EncWriter) Flush() (int, error) { return w.length, w.err }

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

42
43
44
45
46
47
48





49
50














51
52


53
54
55
56
57
58



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




1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

20
21
22
23
24


25
26
27
28
29
30
31


32

33
34
35
36
37

38
39
40
41
42





43
44
45
46
47
48

49
50

51
52
53
54
55
56


57
58
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

-
+

-
+




+
+
+








-
+

+
+

-
-
+
+
+
+



-
-
+
-


+
+
+
-
+

+
+

-
-
-
-
-
+
+
+
+
+

-
+

-
+





-
-
+
+
+
+
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+



-
-
-
+
+
+





-
-
-
+
+
+






+
-
-
-
+
+
+
+


-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
+
+
-
-
+


-
+





-
-
+

-
+

-
+

-
+


+
+
-
-
+
+
+
+


-
+

-


-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+

-


-
+





-
-
-
-
+
+
+
+
+
+


-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
+



+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+

-
+
-


-
-
-
+
+
+
+
+
+

-











-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
+
+
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+










-
+

-
+











-
-
-
-
-
-
-
-
-
-
-
-
+





-
-
-
-
-
+
+
+
-
-
-
-
+

-
-
+
+

+
+
+




-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
+
+





-
-
-
-
-
+
+
+
+
-
-
+
+
+

-
-
-


-
+
-
-
+
-
-
+
-
-
+
-
-
-
-
+
+
+
-
-
-
-
+
+
+
-


-
-
-
+
+

-
+

+
-
-
+
+
-
-
-
-
+
+

-
-
-
-
-
-
-
-
-
+
+
+
+

-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+

-
+



-
+

-
+

-
+
-
-
-
-
-
-
-
-
-
-
-



-
-
+
+


-
-
-
-
-
-

-
+








-
+









-
+








+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zmkenc encodes the abstract syntax tree back into Zettelmarkup.
package zmkenc

import (
	"fmt"
	"io"
	"sort"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register("zmk", encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &zmkEncoder{} },
	encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
	})
}

// Create an encoder.
func Create() *Encoder { return &myZE }

type zmkEncoder struct{}
type Encoder struct{}

var myZE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (ze *zmkEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newVisitor(w, ze)
	if inhMeta {
		zn.InhMeta.WriteAsHeader(&v.b, true)
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	v.acceptMeta(zn.InhMeta, evalMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		zn.Meta.WriteAsHeader(&v.b, true)
		v.b.WriteByte('\n')
	}
	v.acceptBlockSlice(zn.Ast)
	ast.Walk(v, &zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as zmk.
func (ze *zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	return m.Write(w, true)
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	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')
	}
}

func (ze *zmkEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, zn.Ast)
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 (ze *zmkEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
	v := newVisitor(w, ze)
	v.acceptBlockSlice(bs)
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
	ast.Walk(v, bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (ze *zmkEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
	v := newVisitor(w, ze)
	v.acceptInlineSlice(is)
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(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
	b      encoder.BufWriter
	prefix []byte
	enc    *zmkEncoder
	textEnc   encoder.Encoder
	prefix    []byte
	inVerse   bool
	inlinePos int
}

func newVisitor(w io.Writer, enc *zmkEncoder) *visitor {
	return &visitor{
func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewEncWriter(w), textEnc: textenc.Create()}
		b:   encoder.NewBufWriter(w),
		enc: enc,
	}
}

// VisitPara emits HTML code for a paragraph: <p>...</p>
func (v *visitor) VisitPara(pn *ast.ParaNode) {
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.InlineSlice:
		for i, in := range *n {
			v.inlinePos = i
			ast.Walk(v, in)
		}
		v.inlinePos = 0
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
	v.acceptInlineSlice(pn.Inlines)
	v.b.WriteByte('\n')
	if len(v.prefix) == 0 {
		v.b.WriteByte('\n')
	}
}

// VisitVerbatim emits HTML code for verbatim lines.
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	// TODO: scan cn.Lines to find embedded "`"s at beginning
	v.b.WriteString("```")
	v.visitAttributes(vn.Attrs)
		v.b.WriteString("---")
		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.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:
		v.visitEmbedBLOB(n)
	case *ast.CiteNode:
		v.visitCite(n)
	case *ast.FootnoteNode:
		v.b.WriteString("[^")
		ast.Walk(v, &n.Inlines)
		v.b.WriteByte(']')
		v.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		v.visitMark(n)
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
	var lastWasParagraph bool
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteByte('\n')
			if lastWasParagraph && !v.inVerse {
				if _, ok := bn.(*ast.ParaNode); ok {
					v.b.WriteByte('\n')
				}
			}
		}
		ast.Walk(v, bn)
		_, lastWasParagraph = bn.(*ast.ParaNode)
	}
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "@@@",
	ast.VerbatimComment: "%%%",
	ast.VerbatimHTML:    "@@@", // Attribute is set to {="html"}
	ast.VerbatimProg:    "```",
	ast.VerbatimEval:    "~~~",
	ast.VerbatimMath:    "$$$",
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind))
	}
	attrs := vn.Attrs
	if vn.Kind == ast.VerbatimHTML {
		attrs = syntaxToHTML(attrs)
	}

	// TODO: scan cn.Lines to find embedded kind[0]s at beginning
	v.b.WriteString(kind)
	v.visitAttributes(attrs)
	v.b.WriteByte('\n')
	for _, line := range vn.Lines {
		v.b.WriteStrings(line, "\n")
	v.b.Write(vn.Content)
	v.b.WriteByte('\n')
	}
	v.b.WriteString("```\n")
	v.b.WriteString(kind)
}

var regionCode = map[ast.RegionCode]string{
var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  ":::",
	ast.RegionQuote: "<<<",
	ast.RegionVerse: "\"\"\"",
}

// VisitRegion writes HTML code for block regions.
func (v *visitor) VisitRegion(rn *ast.RegionNode) {
func (v *visitor) visitRegion(rn *ast.RegionNode) {
	// Scan rn.Blocks for embedded regions to adjust length of regionCode
	code, ok := regionCode[rn.Code]
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown region code %d", rn.Code))
		panic(fmt.Sprintf("Unknown region kind %d", rn.Kind))
	}
	v.b.WriteString(code)
	v.b.WriteString(kind)
	v.visitAttributes(rn.Attrs)
	v.b.WriteByte('\n')
	saveInVerse := v.inVerse
	v.inVerse = rn.Kind == ast.RegionVerse
	v.acceptBlockSlice(rn.Blocks)
	v.b.WriteString(code)
	ast.Walk(v, &rn.Blocks)
	v.inVerse = saveInVerse
	v.b.WriteByte('\n')
	v.b.WriteString(kind)
	if len(rn.Inlines) > 0 {
		v.b.WriteByte(' ')
		v.acceptInlineSlice(rn.Inlines)
		ast.Walk(v, &rn.Inlines)
	}
	v.b.WriteByte('\n')
}

// VisitHeading writes the HTML code for a heading.
func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	for i := 0; i <= hn.Level; i++ {
		v.b.WriteByte('=')
	}
	v.b.WriteByte(' ')
	v.acceptInlineSlice(hn.Inlines)
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte('\n')
}

	const headingSigns = "========= "
// VisitHRule writes HTML code for a horizontal rule: <hr>.
func (v *visitor) VisitHRule(hn *ast.HRuleNode) {
	v.b.WriteString("---")
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
	ast.Walk(v, &hn.Inlines)
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte('\n')
}

var listCode = map[ast.NestedListCode]byte{
var mapNestedListKind = map[ast.NestedListKind]byte{
	ast.NestedListOrdered:   '#',
	ast.NestedListUnordered: '*',
	ast.NestedListQuote:     '>',
}

// VisitNestedList writes HTML code for lists and blockquotes.
func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
	v.prefix = append(v.prefix, listCode[ln.Code])
	for _, item := range ln.Items {
func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	v.prefix = append(v.prefix, mapNestedListKind[ln.Kind])
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.b.Write(v.prefix)
		v.b.WriteByte(' ')
		for i, in := range item {
			if i > 0 {
		for j, in := range item {
			if j > 0 {
				if _, ok := in.(*ast.ParaNode); ok {
					v.b.WriteByte('\n')
					for j := 0; j <= len(v.prefix); j++ {
						v.b.WriteByte(' ')
					}
				}
				v.b.WriteByte('\n')
				if _, ok := in.(*ast.ParaNode); ok {
					v.writePrefixSpaces()
				}
			}
			}
			in.Accept(v)
			ast.Walk(v, in)
		}
	}
	v.prefix = v.prefix[:len(v.prefix)-1]
}

func (v *visitor) writePrefixSpaces() {
	if prefixLen := len(v.prefix); prefixLen > 0 {
		for i := 0; i <= prefixLen; i++ {
	v.b.WriteByte('\n')
}

// VisitDescriptionList emits a HTML description list.
func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	for _, descr := range dn.Descriptions {
			v.b.WriteByte(' ')
		}
	}
}

func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	for i, descr := range dn.Descriptions {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.b.WriteString("; ")
		v.acceptInlineSlice(descr.Term)
		ast.Walk(v, &descr.Term)
		v.b.WriteByte('\n')

		for _, b := range descr.Descriptions {
			v.b.WriteString(": ")
			for _, dn := range b {
				dn.Accept(v)
			v.b.WriteString("\n: ")
			for jj, dn := range b {
				if jj > 0 {
					v.b.WriteString("\n\n  ")
				}
				ast.Walk(v, dn)
			}
			v.b.WriteByte('\n')
		}
	}
}

var alignCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
	ast.AlignCenter:  ":",
	ast.AlignRight:   ">",
}

// VisitTable emits a HTML table.
func (v *visitor) VisitTable(tn *ast.TableNode) {
	if len(tn.Header) > 0 {
		for pos, cell := range tn.Header {
			v.b.WriteString("|=")
			colAlign := tn.Align[pos]
			if cell.Align != colAlign {
				v.b.WriteString(alignCode[cell.Align])
			}
func (v *visitor) visitTable(tn *ast.TableNode) {
	if header := tn.Header; len(header) > 0 {
		v.writeTableHeader(header, tn.Align)
		v.b.WriteByte('\n')
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte('\n')
		}
			v.acceptInlineSlice(cell.Inlines)
			if colAlign != ast.AlignDefault {
		v.writeTableRow(row, tn.Align)
				v.b.WriteString(alignCode[colAlign])
			}
		}
	}
}
		v.b.WriteByte('\n')
	}

	for _, row := range tn.Rows {
		for pos, cell := range row {
			v.b.WriteByte('|')
			if cell.Align != tn.Align[pos] {
				v.b.WriteString(alignCode[cell.Align])
			}
			v.acceptInlineSlice(cell.Inlines)
func (v *visitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
	for pos, cell := range header {
		v.b.WriteString("|=")
		colAlign := align[pos]
		if cell.Align != colAlign {
			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, &cell.Inlines)
		}
		v.b.WriteByte('\n')
	}
		if colAlign != ast.AlignDefault {
			v.b.WriteString(alignCode[colAlign])
		}
	v.b.WriteByte('\n')
}

// VisitBLOB writes the binary object as a value.
func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {
	v.b.WriteStrings(
		"%% Unable to display BLOB with title '",
	}
}

func (v *visitor) writeTableRow(row ast.TableRow, align []ast.Alignment) {
	for pos, cell := range row {
		v.b.WriteByte('|')
		if cell.Align != align[pos] {
			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, &cell.Inlines)
	}
}

		bn.Title,
		"' and syntax '",
		bn.Syntax,
		"'\n")
}

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 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 = map[string]bool{
	"\\":   true,
	"//":   true,
	"**":   true,
	"__":   true,
	"~~":   true,
	"^^":   true,
	",,":   true,
	"<<":   true,
	"\"\"": true,
	";;":   true,
	"::":   true,
	"''":   true,
	"``":   true,
	"++":   true,
	"==":   true,
}

// VisitText writes text content.
func (v *visitor) VisitText(tn *ast.TextNode) {

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 _, ok := escapeSeqs[s]; ok {
			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
			}
		}
	}
	v.b.WriteString(tn.Text[last:])
}

// VisitTag writes tag content.
func (v *visitor) VisitTag(tn *ast.TagNode) {
	v.b.WriteStrings("#", tn.Tag)
}

// VisitSpace emits a white space.
func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
	v.b.WriteString(sn.Lexeme)
}

// VisitBreak writes HTML code for line breaks.
func (v *visitor) VisitBreak(bn *ast.BreakNode) {
func (v *visitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteByte('\n')
	}
	if prefixLen := len(v.prefix); prefixLen > 0 {
		for i := 0; i <= prefixLen; i++ {
			v.b.WriteByte(' ')
		}
	}
	v.writePrefixSpaces()
}

}

// VisitLink writes HTML code for links.
func (v *visitor) VisitLink(ln *ast.LinkNode) {
func (v *visitor) visitLink(ln *ast.LinkNode) {
	v.b.WriteString("[[")
	if !ln.OnlyRef {
		v.acceptInlineSlice(ln.Inlines)
	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(), "]]")
}

// VisitImage writes HTML code for images.
func (v *visitor) VisitImage(in *ast.ImageNode) {
func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	if in.Ref != nil {
		v.b.WriteString("{{")
		if len(in.Inlines) > 0 {
			v.acceptInlineSlice(in.Inlines)
			v.b.WriteByte('|')
		}
		v.b.WriteStrings(in.Ref.String(), "}}")
	}
}

// VisitCite writes code for citations.
func (v *visitor) VisitCite(cn *ast.CiteNode) {
	v.b.WriteString("{{")
	if len(en.Inlines) > 0 {
		ast.Walk(v, &en.Inlines)
		v.b.WriteByte('|')
	}
	v.b.WriteStrings(en.Ref.String(), "}}")
}

func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	if en.Syntax == meta.SyntaxSVG {
		v.b.WriteString("@@")
		v.b.Write(en.Blob)
		v.b.WriteStrings("@@{=", en.Syntax, "}")
		return
	}
	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.acceptInlineSlice(cn.Inlines)
		v.b.WriteByte(' ')
		ast.Walk(v, &cn.Inlines)
	}
	v.b.WriteByte(']')
	v.visitAttributes(cn.Attrs)
}

// VisitFootnote write HTML code for a footnote.
func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) {
	v.b.WriteString("[^")
	v.acceptInlineSlice(fn.Inlines)
	v.b.WriteByte(']')
func (v *visitor) visitMark(mn *ast.MarkNode) {
	v.b.WriteStrings("[!", mn.Mark)
	if len(mn.Inlines) > 0 {
		v.b.WriteByte('|')
	v.visitAttributes(fn.Attrs)
}
		ast.Walk(v, &mn.Inlines)
	}
	v.b.WriteByte(']')

// VisitMark writes HTML code to mark a position.
func (v *visitor) VisitMark(mn *ast.MarkNode) {
	v.b.WriteStrings("[!", mn.Text, "]")
}

var formatCode = map[ast.FormatCode][]byte{
var mapFormatKind = map[ast.FormatKind][]byte{
	ast.FormatItalic:    []byte("//"),
	ast.FormatEmph:      []byte("//"),
	ast.FormatEmph:   []byte("__"),
	ast.FormatBold:      []byte("**"),
	ast.FormatStrong:    []byte("**"),
	ast.FormatStrong: []byte("**"),
	ast.FormatUnder:     []byte("__"),
	ast.FormatInsert:    []byte("__"),
	ast.FormatInsert: []byte(">>"),
	ast.FormatStrike:    []byte("~~"),
	ast.FormatDelete:    []byte("~~"),
	ast.FormatSuper:     []byte("^^"),
	ast.FormatSub:       []byte(",,"),
	ast.FormatDelete: []byte("~~"),
	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuotation: []byte("<<"),
	ast.FormatQuote:     []byte("\"\""),
	ast.FormatSmall:     []byte(";;"),
	ast.FormatSpan:      []byte("::"),
	ast.FormatQuote:  []byte(`""`),
	ast.FormatMark:   []byte("##"),
	ast.FormatSpan:   []byte("::"),
	ast.FormatMonospace: []byte("''"),
}

// VisitFormat write HTML code for formatting text.
func (v *visitor) VisitFormat(fn *ast.FormatNode) {
	code, ok := formatCode[fn.Code]
func (v *visitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown format code %d", fn.Code))
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))
	}
	v.b.Write(kind)
	attrs := fn.Attrs
	switch fn.Code {
	ast.Walk(v, &fn.Inlines)
	v.b.Write(kind)
	case ast.FormatEmph, ast.FormatStrong, ast.FormatInsert, ast.FormatDelete:
		attrs = attrs.Clone()
		attrs.Set("-", "")
	}
	v.visitAttributes(fn.Attrs)
}

	v.b.Write(code)
	v.acceptInlineSlice(fn.Inlines)
	v.b.Write(code)
	v.visitAttributes(attrs)
}

// VisitLiteral write Zettelmarkup for inline literal text.
func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
	switch ln.Code {
func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralZettel:
		v.writeLiteral('@', ln.Attrs, ln.Content)
	case ast.LiteralProg:
		v.writeLiteral('`', ln.Attrs, ln.Text)
	case ast.LiteralKeyb:
		v.writeLiteral('+', ln.Attrs, ln.Text)
	case ast.LiteralOutput:
		v.writeLiteral('=', ln.Attrs, ln.Text)
	case ast.LiteralComment:
		v.b.WriteStrings("%% ", ln.Text)
	case ast.LiteralHTML:
		v.b.WriteString("``")
		v.writeLiteral('`', ln.Attrs, ln.Content)
	case ast.LiteralMath:
		v.b.WriteStrings("$$", string(ln.Content), "$$")
		v.visitAttributes(ln.Attrs)
	case ast.LiteralInput:
		v.writeLiteral('\'', ln.Attrs, ln.Content)
	case ast.LiteralOutput:
		v.writeLiteral('=', ln.Attrs, ln.Content)
	case ast.LiteralComment:
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
		v.writeEscaped(ln.Text, '`')
		v.b.WriteString("``{=html,.warning}")
		}
		v.b.WriteString("%%")
		v.visitAttributes(ln.Attrs)
		v.b.WriteByte(' ')
		v.b.Write(ln.Content)
	case ast.LiteralHTML:
		v.writeLiteral('@', syntaxToHTML(ln.Attrs), ln.Content)
	default:
		panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
	}
}

func (v *visitor) writeLiteral(code byte, attrs *ast.Attributes, text string) {
func (v *visitor) writeLiteral(code byte, a attrs.Attributes, content []byte) {
	v.b.WriteBytes(code, code)
	v.writeEscaped(text, code)
	v.writeEscaped(string(content), code)
	v.b.WriteBytes(code, code)
	v.visitAttributes(attrs)
	v.visitAttributes(a)
}

func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
	for _, bn := range bns {
		bn.Accept(v)
	}
}
func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
	for _, in := range ins {
		in.Accept(v)
	}
}

// visitAttributes write HTML attributes
func (v *visitor) visitAttributes(a *ast.Attributes) {
	if a == nil || len(a.Attrs) == 0 {
func (v *visitor) visitAttributes(a attrs.Attributes) {
	if a.IsEmpty() {
		return
	}
	keys := make([]string, 0, len(a.Attrs))
	for k := range a.Attrs {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	v.b.WriteByte('{')
	for i, k := range keys {
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteByte(' ')
		}
		if k == "-" {
			v.b.WriteByte('-')
			continue
		}
		v.b.WriteString(k)
		if vl := a.Attrs[k]; len(vl) > 0 {
		if vl := a[k]; len(vl) > 0 {
			v.b.WriteStrings("=\"", vl)
			v.b.WriteByte('"')
		}
	}
	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)
}

Added 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
117
118
119
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 atom provides an Atom encoding.
package atom

import (
	"bytes"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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.TimestampLayout, val, time.Local); err == nil {
			entryUpdated = published.UTC().Format(time.RFC3339)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).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")
			}
		}
	}
}

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 encoding provides helper functions for encodings.
package encoding

import (
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
	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.TimestampLayout, val, time.Local); err == nil {
				if maxPublished.Before(published) {
					maxPublished = published
				}
			}
		}
	}
	if maxPublished.Year() > 1 {
		return maxPublished.UTC().Format(timeFormat)
	}
	return ""
}

// TitleAsText returns the title of a zettel as plain text
func TitleAsText(m *meta.Meta) string { return parser.NormalizedSpacedText(m.GetTitle()) }

Added 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
119
120
121
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 rss provides a RSS encoding.
package rss

import (
	"bytes"
	"context"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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.TimestampLayout, val, time.Local); err == nil {
			itemPublished = published.UTC().Format(time.RFC1123Z)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).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)
			}
		}
	}
}

Added 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
34
35
36
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 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")
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/sx.fossil/sxbuiltins"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/parser/draw"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
type Port interface {
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}

// EvaluateZettel evaluates the given zettel in the given context, with the
// given ports, and the given environment.
func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) {
	switch zn.Syntax {
	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 result
				}
			}
		}
	}
	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)
}

// EvaluateInline evaluates the given inline list in the given context, with
// the given ports, and the given environment.
func EvaluateInline(ctx context.Context, port Port, rtConfig config.Config, is *ast.InlineSlice) {
	evaluateNode(ctx, port, rtConfig, is)
	cleaner.CleanInlineSlice(is)
}

func evaluateNode(ctx context.Context, port Port, rtConfig config.Config, n ast.Node) {
	e := evaluator{
		ctx:             ctx,
		port:            port,
		rtConfig:        rtConfig,
		transcludeMax:   rtConfig.GetMaxTransclusions(),
		transcludeCount: 0,
		costMap:         map[id.Zid]transcludeCost{},
		embedMap:        map[string]ast.InlineSlice{},
		marker:          &ast.ZettelNode{},
	}
	ast.Walk(&e, n)
}

type evaluator struct {
	ctx             context.Context
	port            Port
	rtConfig        config.Config
	transcludeMax   int
	transcludeCount int
	costMap         map[id.Zid]transcludeCost
	marker          *ast.ZettelNode
	embedMap        map[string]ast.InlineSlice
}

type transcludeCost struct {
	zn *ast.ZettelNode
	ec int
}

func (e *evaluator) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		e.visitBlockSlice(n)
	case *ast.InlineSlice:
		e.visitInlineSlice(n)
	default:
		return e
	}
	return nil
}

func (e *evaluator) visitBlockSlice(bs *ast.BlockSlice) {
	for i := 0; i < len(*bs); i++ {
		bn := (*bs)[i]
		ast.Walk(e, bn)
		switch n := bn.(type) {
		case *ast.VerbatimNode:
			i += transcludeNode(bs, i, e.evalVerbatimNode(n))
		case *ast.TranscludeNode:
			i += transcludeNode(bs, i, e.evalTransclusionNode(n))
		}
	}
}

func transcludeNode(bln *ast.BlockSlice, i int, bn ast.BlockNode) int {
	if ln, ok := bn.(*ast.BlockSlice); ok {
		*bln = replaceWithBlockNodes(*bln, i, *ln)
		return len(*ln) - 1
	}
	if bn == nil {
		(*bln) = (*bln)[:i+copy((*bln)[i:], (*bln)[i+1:])]
		return -1
	}
	(*bln)[i] = bn
	return 0
}

func replaceWithBlockNodes(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode {
	if len(replaceBns) == 1 {
		bns[i] = replaceBns[0]
		return bns
	}
	newIns := make([]ast.BlockNode, 0, len(bns)+len(replaceBns)-1)
	if i > 0 {
		newIns = append(newIns, bns[:i]...)
	}
	if len(replaceBns) > 0 {
		newIns = append(newIns, replaceBns...)
	}
	if i+1 < len(bns) {
		newIns = append(newIns, bns[i+1:]...)
	}
	return newIns
}

func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode {
	switch vn.Kind {
	case ast.VerbatimZettel:
		return e.evalVerbatimZettel(vn)
	case ast.VerbatimEval:
		if syntax, found := vn.Attrs.Get(""); found && syntax == meta.SyntaxDraw {
			return draw.ParseDrawBlock(vn)
		}
	}
	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 := zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent(vn.Content),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	return &zn.Ast
}

func getSyntax(a attrs.Attributes, defSyntax string) string {
	if a != nil {
		if val, ok := a.Get(api.KeySyntax); ok {
			return val
		}
		if val, ok := a.Get(""); ok {
			return val
		}
	}
	return defSyntax
}

func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode {
	ref := tn.Ref

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
		return makeBlockNode(errText)
	}
	switch ref.State {
	case ast.RefStateZettel:
		// Only zettel references will be evaluated.
	case ast.RefStateInvalid, ast.RefStateBroken:
		e.transcludeCount++
		return makeBlockNode(createInlineErrorText(ref, "Invalid", "or", "broken", "transclusion", "reference"))
	case ast.RefStateSelf:
		e.transcludeCount++
		return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference"))
	case ast.RefStateFound, ast.RefStateExternal:
		return tn
	case ast.RefStateHosted, ast.RefStateBased:
		if n := createEmbeddedNodeLocal(ref); n != nil {
			n.Attrs = tn.Attrs
			return makeBlockNode(n)
		}
		return tn
	case ast.RefStateQuery:
		e.transcludeCount++
		return e.evalQueryTransclusion(tn.Ref.Value)
	default:
		return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State))))
	}

	zid, err := id.Parse(ref.URL.Path)
	if err != nil {
		panic(err)
	}

	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return makeBlockNode(createInlineErrorText(ref, "Recursive", "transclusion"))
	}
	if !ok {
		zettel, err1 := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
		if err1 != nil {
			if errors.Is(err1, &box.ErrNotAllowed{}) {
				return nil
			}
			e.transcludeCount++
			return makeBlockNode(createInlineErrorText(ref, "Unable", "to", "get", "zettel"))
		}
		setMetadataFromAttributes(zettel.Meta, tn.Attrs)
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
		e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
	}
	e.transcludeCount++
	if ec := cost.ec; ec > 0 {
		e.transcludeCount += cost.ec
	}
	return &zn.Ast
}

func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode {
	q := query.Parse(expr)
	ml, err := e.port.QueryMeta(e.ctx, q)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
	}
	result, _ := QueryAction(e.ctx, q, ml, e.rtConfig)
	if result != nil {
		ast.Walk(e, result)
	}
	return result
}

func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {
	if maxTrans := e.transcludeMax; e.transcludeCount > maxTrans {
		e.transcludeCount = maxTrans + 1
		return createInlineErrorText(ref,
			"Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",",
			"see", "runtime", "configuration", "key", "max-transclusions)")
	}
	return nil
}

func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) }

func setMetadataFromAttributes(m *meta.Meta, a attrs.Attributes) {
	for aKey, aVal := range a {
		if meta.KeyIsValid(aKey) {
			m.Set(aKey, aVal)
		}
	}
}

func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) {
	for i := 0; i < len(*is); i++ {
		in := (*is)[i]
		ast.Walk(e, in)
		switch n := in.(type) {
		case *ast.LinkNode:
			(*is)[i] = e.evalLinkNode(n)
		case *ast.EmbedRefNode:
			i += embedNode(is, i, e.evalEmbedRefNode(n))
		case *ast.LiteralNode:
			i += embedNode(is, i, e.evalLiteralNode(n))
		}
	}
}

func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int {
	if ln, ok := in.(*ast.InlineSlice); ok {
		*is = replaceWithInlineNodes(*is, i, *ln)
		return len(*ln) - 1
	}
	if in == nil {
		(*is) = (*is)[:i+copy((*is)[i:], (*is)[i+1:])]
		return -1
	}
	(*is)[i] = in
	return 0
}

func replaceWithInlineNodes(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice {
	if len(replaceIns) == 1 {
		ins[i] = replaceIns[0]
		return ins
	}
	newIns := make(ast.InlineSlice, 0, len(ins)+len(replaceIns)-1)
	if i > 0 {
		newIns = append(newIns, ins[:i]...)
	}
	if len(replaceIns) > 0 {
		newIns = append(newIns, replaceIns...)
	}
	if i+1 < len(ins) {
		newIns = append(newIns, ins[i+1:]...)
	}
	return newIns
}

func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode {
	if len(ln.Inlines) == 0 {
		ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}}
	}
	ref := ln.Ref
	if ref == nil || ref.State != ast.RefStateZettel {
		return ln
	}

	zid := mustParseZid(ref)
	_, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if errors.Is(err, &box.ErrNotAllowed{}) {
		return &ast.FormatNode{
			Kind:    ast.FormatSpan,
			Attrs:   ln.Attrs,
			Inlines: getLinkInline(ln),
		}
	} else if err != nil {
		ln.Ref.State = ast.RefStateBroken
		return ln
	}

	ln.Ref.State = ast.RefStateZettel
	return ln
}

func getLinkInline(ln *ast.LinkNode) ast.InlineSlice {
	if ln.Inlines != nil {
		return ln.Inlines
	}
	return ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}}
}

func (e *evaluator) evalEmbedRefNode(en *ast.EmbedRefNode) ast.InlineNode {
	ref := en.Ref

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
		return errText
	}

	switch ref.State {
	case ast.RefStateZettel:
		// Only zettel references will be evaluated.
	case ast.RefStateInvalid, ast.RefStateBroken:
		e.transcludeCount++
		return createInlineErrorImage(en)
	case ast.RefStateSelf:
		e.transcludeCount++
		return createInlineErrorText(ref, "Self", "embed", "reference")
	case ast.RefStateFound, ast.RefStateExternal:
		return en
	case ast.RefStateHosted, ast.RefStateBased:
		if n := createEmbeddedNodeLocal(ref); n != nil {
			n.Attrs = en.Attrs
			n.Inlines = en.Inlines
			return n
		}
		return en
	default:
		return createInlineErrorText(ref, "Illegal", "inline", "state", strconv.Itoa(int(ref.State)))
	}

	zid := mustParseZid(ref)
	zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		e.transcludeCount++
		return createInlineErrorImage(en)
	}

	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+")")
	}

	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return createInlineErrorText(ref, "Recursive", "transclusion")
	}
	if !ok {
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
		e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
	}
	e.transcludeCount++

	result, ok := e.embedMap[ref.Value]
	if !ok {
		// Search for text to be embedded.
		result = findInlineSlice(&zn.Ast, ref.URL.Fragment)
		e.embedMap[ref.Value] = result
	}
	if len(result) == 0 {
		return &ast.LiteralNode{
			Kind:    ast.LiteralComment,
			Attrs:   map[string]string{"-": ""},
			Content: append([]byte("Nothing to transclude: "), ref.String()...),
		}
	}

	if ec := cost.ec; ec > 0 {
		e.transcludeCount += cost.ec
	}
	return &result
}

func mustParseZid(ref *ast.Reference) id.Zid {
	zid, err := id.Parse(ref.URL.Path)
	if err != nil {
		panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref))
	}
	return zid
}

func (e *evaluator) updateImageRefNode(en *ast.EmbedRefNode, m *meta.Meta, syntax string) {
	en.Syntax = syntax
	if len(en.Inlines) == 0 {
		is := parser.ParseDescription(m)
		if len(is) > 0 {
			ast.Walk(e, &is)
			if len(is) > 0 {
				en.Inlines = is
			}
		}
	}
}

func (e *evaluator) evalLiteralNode(ln *ast.LiteralNode) ast.InlineNode {
	if ln.Kind != ast.LiteralZettel {
		return ln
	}
	e.transcludeCount++
	result := e.evaluateEmbeddedInline(ln.Content, getSyntax(ln.Attrs, meta.SyntaxText))
	if len(result) == 0 {
		return &ast.LiteralNode{
			Kind:    ast.LiteralComment,
			Attrs:   map[string]string{"-": ""},
			Content: []byte("Nothing to transclude"),
		}
	}
	return &result
}

func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode {
	errorZid := id.EmojiZid
	en.Ref = ast.ParseReference(errorZid.String())
	if len(en.Inlines) == 0 {
		en.Inlines = parser.ParseMetadata("Error placeholder")
	}
	return en
}

func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode {
	text := strings.Join(msgWords, " ")
	if ref != nil {
		text += ": " + ref.String() + "."
	}
	ln := &ast.LiteralNode{
		Kind:    ast.LiteralInput,
		Content: []byte(text),
	}
	fn := &ast.FormatNode{
		Kind:    ast.FormatStrong,
		Inlines: ast.InlineSlice{ln},
	}
	fn.Attrs = fn.Attrs.AddClass("error")
	return fn
}

func createEmbeddedNodeLocal(ref *ast.Reference) *ast.EmbedRefNode {
	ext := path.Ext(ref.Value)
	if ext != "" && ext[0] == '.' {
		ext = ext[1:]
	}
	pinfo := parser.Get(ext)
	if pinfo == nil || !pinfo.IsImageFormat {
		return nil
	}
	return &ast.EmbedRefNode{
		Ref:    ref,
		Syntax: ext,
	}
}

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 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)
	}
	fs := fragmentSearcher{fragment: fragment}
	ast.Walk(&fs, bs)
	return fs.result
}

func firstInlinesToEmbed(bs ast.BlockSlice) ast.InlineSlice {
	if ins := bs.FirstParagraphInlines(); ins != nil {
		return ins
	}
	if len(bs) == 0 {
		return nil
	}
	if bn, ok := bs[0].(*ast.BLOBNode); ok {
		return ast.InlineSlice{&ast.EmbedBLOBNode{
			Blob:    bn.Blob,
			Syntax:  bn.Syntax,
			Inlines: bn.Description,
		}}
	}
	return nil
}

type fragmentSearcher struct {
	fragment string
	result   ast.InlineSlice
}

func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor {
	if len(fs.result) > 0 {
		return nil
	}
	switch n := node.(type) {
	case *ast.BlockSlice:
		fs.visitBlockSlice(n)
	case *ast.InlineSlice:
		fs.visitInlineSlice(n)
	default:
		return fs
	}
	return nil
}

func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment {
			fs.result = (*bs)[i+1:].FirstParagraphInlines()
			return
		}
		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:])
			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, ris...)
			} else {
				fs.result = ris
			}
			return
		}
		ast.Walk(fs, in)
	}
}

func skipSpaceNodes(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
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// 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, int) {
	ap := actionPara{
		ctx:   ctx,
		q:     q,
		ml:    ml,
		kind:  ast.NestedListUnordered,
		min:   -1,
		max:   -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, api.NumberedAction[0:1]) {
			ap.kind = ast.NestedListOrdered
			continue
		}
		if strings.HasPrefix(act, api.MinAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.min = num
				continue
			}
		}
		if strings.HasPrefix(act, api.MaxAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.max = num
				continue
			}
		}
		if act == api.TitleAction && i+1 < len(actions) {
			ap.title = strings.Join(actions[i+1:], " ")
			break
		}
		if act == api.ReIndexAction {
			continue
		}
		acts = append(acts, act)
	}
	var firstUnknowAct string
	for _, act := range acts {
		switch act {
		case api.AtomAction:
			return ap.createBlockNodeAtom(rtConfig)
		case api.RSSAction:
			return ap.createBlockNodeRSS(rtConfig)
		case api.KeysAction:
			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 firstUnknowAct == "" {
			firstUnknowAct = act
		}
	}
	bn, numItems := ap.createBlockNodeMeta(strings.ToLower(firstUnknowAct))
	if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) {
		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
	title string
}

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, 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, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		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: " "})
		}
		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}, 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 max < 0 {
			max = ccs[0].Count
		}
		if ccs[len(ccs)-1].Count < min || max < ccs[0].Count {
			temp := make(meta.CountedCategories, 0, len(ccs))
			for _, cat := range ccs {
				if min <= cat.Count && cat.Count <= max {
					temp = append(temp, cat)
				}
			}
			return temp
		}
	}
	return ccs
}

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, 0
	}
	ccs := arr.Counted()
	ccs.SortByName()

	var buf bytes.Buffer
	bufLen := ap.prepareSimpleQuery(&buf)
	items := make([]ast.ItemSlice, 0, len(ccs))
	for _, cat := range ccs {
		buf.WriteString(cat.Name)
		buf.WriteString(api.ExistOperator)
		q1 := buf.String()
		buf.Truncate(bufLen)
		buf.WriteString(api.ActionSeparator)
		buf.WriteString(cat.Name)
		q2 := buf.String()
		buf.Truncate(bufLen)

		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: "(" + 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, int) {
	if len(ap.ml) == 0 {
		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
			}
		}
		items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
			Attrs:   nil,
			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()
	if len(ccs) == 0 {
		return nil, 0
	}

	ap.prepareSimpleQuery(buf)
	buf.WriteString(key)
	buf.WriteString(api.SearchOperatorHas)
	bufLen := buf.Len()

	return ccs, bufLen
}

func (ap *actionPara) prepareSimpleQuery(buf *bytes.Buffer) int {
	sea := ap.q.Clone()
	sea.RemoveActions()
	buf.WriteString(ast.QueryPrefix)
	sea.Print(buf)
	if buf.Len() > len(ast.QueryPrefix) {
		buf.WriteByte(' ')
	}
	return buf.Len()
}

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 := 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)

	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]
			curSize++
		}
		return result
	}

	// Idea: the number of occurences for a specific count is substracted from a budget.
	total := float64(len(ccs))
	curSize := 0
	budget := calcBudget(total, 0.0)
	for _, count := range countList {
		result[count] = fsAttrs[curSize]
		cc := float64(countMap[count])
		total -= cc
		budget -= cc
		if budget < 1 {
			curSize++
			if curSize >= fontSizes {
				curSize = fontSizes
				budget = 0.0
			} else {
				budget = calcBudget(total, float64(curSize))
			}
		}
	}
	return result
}

func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) }

func (ap *actionPara) createBlockNodeRSS(cfg config.Config) (ast.BlockNode, int) {
	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,
	}, len(ap.ml)
}

func (ap *actionPara) createBlockNodeAtom(cfg config.Config) (ast.BlockNode, int) {
	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,
	}, len(ap.ml)
}

Added 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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package evaluator

import (
	"zettelstore.de/z/ast"
	"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))
	}
	return ast.BlockSlice{descrlist}
}

func getMetadataDescription(key, value string) ast.Description {
	is := convertMetavalueToInlineSlice(value, meta.Type(key))
	return ast.Description{
		Term:         ast.InlineSlice{&ast.TextNode{Text: key}},
		Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}},
	}
}

func convertMetavalueToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice {
	var sliceData []string
	if dt.IsSet {
		sliceData = meta.ListFromValue(value)
		if len(sliceData) == 0 {
			return nil
		}
	} else {
		sliceData = []string{value}
	}
	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: " "})
		}
		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


1
2

3
4
5

6





7
8
9
10
11
12
13
14
15


-
+


-
+
-
-
-
-
-
+
+
+
+
+
+

+
+
module zettelstore.de/z

go 1.16
go 1.22

require (
	github.com/fsnotify/fsnotify v1.4.9
	github.com/fsnotify/fsnotify v1.7.0
	github.com/pascaldekloe/jwt v1.10.0
	github.com/yuin/goldmark v1.3.7
	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
	golang.org/x/term v0.0.0-20201117132131-f5c789dd3221
	golang.org/x/text v0.3.6
	github.com/yuin/goldmark v1.7.0
	golang.org/x/crypto v0.21.0
	golang.org/x/term v0.18.0
	golang.org/x/text v0.14.0
	zettelstore.de/client.fossil v0.0.0-20240318100248-bbbd4f880571
	zettelstore.de/sx.fossil v0.0.0-20240316123622-97ad67098e8a
)

require golang.org/x/sys v0.18.0 // indirect

Changes to go.sum.

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
-
-
+
+
-
-
-
-
-
-
+
+
+
+
-
-
-
+
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs=
github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A=
github.com/yuin/goldmark v1.3.7 h1:NSaHgaeJFCtWXCBkBKXw0rhgMuJ0VoE9FB5mWldcrQ4=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
zettelstore.de/client.fossil v0.0.0-20240318100248-bbbd4f880571 h1:0x+Lma9+cXI03/EZhm472TIoDGgAps6tzFwDHqkj7+8=
zettelstore.de/client.fossil v0.0.0-20240318100248-bbbd4f880571/go.mod h1:KbTWx3VhIIAtg3Tla93TWUVaNMInnDEL4xgdNv55tkQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
zettelstore.de/sx.fossil v0.0.0-20240316123622-97ad67098e8a h1:4t/TbT5X3KTMQSa3TQdACxzgoo+V450MgMy/maqWoos=
zettelstore.de/sx.fossil v0.0.0-20240316123622-97ad67098e8a/go.mod h1:/iGHxFXoo6GSV04PUkwaLuFrrCa5LMorxD73iLMAruI=

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
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-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package input provides an abstraction for data to be read.
package input

import (
	"html"
	"unicode"
	"unicode/utf8"
)

// Input is an abstract input source
type Input struct {
	// Read-only, will never change
	Src string // 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 string) *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.
func (inp *Input) Next() {
	if inp.readPos < len(inp.Src) {
		inp.Pos = inp.readPos
		r, w := rune(inp.Src[inp.readPos]), 1
		if r >= utf8.RuneSelf {
			r, w = utf8.DecodeRuneInString(inp.Src[inp.readPos:])
		}
		inp.readPos += w
		inp.Ch = r
	} else {
		inp.Pos = len(inp.Src)
		inp.Ch = EOS
	}
}

// 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.DecodeRuneInString(inp.Src[pos:])
		}
		if r == '\t' {
			return ' '
		}
		return r
	}
	return EOS
}

// 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) {
	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()
	}
}

// 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()
			return string(rune(code)), true
		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()
			return string(rune(code)), true
		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 := inp.Src[pos:inp.Pos]
			ues := html.UnescapeString(es)
			if es == ues {
				return "", false
			}
			return ues, true
		}
		inp.Next()
	}
}

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
66
67



































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package input_test provides some unit-tests for reading data.
package input_test

import (
	"testing"

	"zettelstore.de/z/input"
)

func TestEatEOL(t *testing.T) {
	inp := input.NewInput("")
	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("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 TestScanEntity(t *testing.T) {
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"a", ""},
		{"&amp;", "&"},
		{"&#9;", "\t"},
		{"&quot;", "\""},
	}
	for id, tc := range testcases {
		inp := input.NewInput(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)
		}
	}
}

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
28



29


30
31
32
33

34
35




36
37
38
39
40
41
42
43


44
45

46
47
48
49
50
51
52
53
54
55
56
57


58

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
83
84
85
86
87
88
89
90
91


92
93
94

95

96
97
98

99



-
+

-
+




+
+
+


-



+



-
-
+
+
+









+
+
+
-
+
+



-
+

-
+
+
+
+






-
-
+
+

-
+












+
+
-
+






-
+


-
+










-
-
+
+

-

-
+


-
+
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"errors"
	"sync"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/kernel"
	"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
}

var errAlreadySetOwner = errors.New("changing an existing owner not allowed")
var errAlreadyROMode = errors.New("system in readonly mode cannot change this mode")

func (as *authService) Initialize() {
func (as *authService) Initialize(logger *logger.Logger) {
	as.logger = logger
	as.descr = descriptionMap{
		kernel.AuthOwner: {
			"Owner's zettel id",
			func(val string) interface{} {
			func(val string) (any, error) {
				if owner := as.cur[kernel.AuthOwner]; owner != nil && owner != id.Invalid {
					return nil
					return nil, errAlreadySetOwner
				}
				if val == "" {
					return id.Invalid, nil
				}
				return parseZid(val)
			},
			false,
		},
		kernel.AuthReadonly: {
			"Read-only mode",
			func(val string) interface{} {
			"Readonly mode",
			func(val string) (any, error) {
				if ro := as.cur[kernel.AuthReadonly]; ro == true {
					return nil
					return nil, errAlreadyROMode
				}
				return parseBool(val)
			},
			true,
		},
	}
	as.next = interfaceMap{
		kernel.AuthOwner:    id.Invalid,
		kernel.AuthReadonly: false,
	}
}

func (as *authService) GetLogger() *logger.Logger { return as.logger }

func (as *authService) Start(kern *myKernel) error {
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 {
		kern.doLog("Unable to create auth manager:", err)
		as.logger.Error().Err(err).Msg("Unable to create manager")
		return err
	}
	kern.doLog("Start Auth Manager")
	as.logger.Info().Msg("Start Manager")
	as.manager = authMgr
	return nil
}

func (as *authService) IsStarted() bool {
	as.mxService.RLock()
	defer as.mxService.RUnlock()
	return as.manager != nil
}

func (as *authService) Stop(kern *myKernel) error {
	kern.doLog("Stop Auth Manager")
func (as *authService) Stop(*myKernel) {
	as.logger.Info().Msg("Stop Manager")
	as.mxService.Lock()
	defer as.mxService.Unlock()
	as.manager = nil
	return nil
	as.mxService.Unlock()
}

func (as *authService) GetStatistics() []kernel.KeyValue {
func (*authService) GetStatistics() []kernel.KeyValue { return nil }
	return nil
}

Added kernel/impl/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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"io"
	"net/url"
	"strconv"
	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
)

type boxService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       box.Manager
	createManager kernel.CreateBoxManagerFunc
}

var errInvalidDirType = errors.New("invalid directory type")

func (ps *boxService) Initialize(logger *logger.Logger) {
	ps.logger = logger
	ps.descr = descriptionMap{
		kernel.BoxDefaultDirType: {
			"Default directory box type",
			ps.noFrozen(func(val string) (any, error) {
				switch val {
				case kernel.BoxDirTypeNotify, kernel.BoxDirTypeSimple:
					return val, nil
				}
				return nil, errInvalidDirType
			}),
			true,
		},
		kernel.BoxURIs: {
			"Box URI",
			func(val string) (any, error) {
				uVal, err := url.Parse(val)
				if err != nil {
					return nil, err
				}
				if uVal.Scheme == "" {
					uVal.Scheme = "dir"
				}
				return uVal, nil
			},
			true,
		},
	}
	ps.next = interfaceMap{
		kernel.BoxDefaultDirType: kernel.BoxDirTypeNotify,
	}
}

func (ps *boxService) GetLogger() *logger.Logger { return ps.logger }

func (ps *boxService) Start(kern *myKernel) error {
	boxURIs := make([]*url.URL, 0, 4)
	for i := 1; ; i++ {
		u := ps.GetNextConfig(kernel.BoxURIs + strconv.Itoa(i))
		if u == nil {
			break
		}
		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.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.Error().Err(err).Msg("Unable to start manager")
		return err
	}
	kern.cfg.setBox(mgr)
	ps.manager = mgr
	return nil
}

func (ps *boxService) IsStarted() bool {
	ps.mxService.RLock()
	defer ps.mxService.RUnlock()
	return ps.manager != nil
}

func (ps *boxService) Stop(*myKernel) {
	ps.logger.Info().Msg("Stop Manager")
	ps.mxService.RLock()
	mgr := ps.manager
	ps.mxService.RUnlock()
	mgr.Stop(context.Background())
	ps.mxService.Lock()
	ps.manager = nil
	ps.mxService.Unlock()
}

func (ps *boxService) GetStatistics() []kernel.KeyValue {
	var st box.Stats
	ps.mxService.RLock()
	ps.manager.ReadStats(&st)
	ps.mxService.RUnlock()
	return []kernel.KeyValue{
		{Key: "Read-only", Value: strconv.FormatBool(st.ReadOnly)},
		{Key: "Managed boxes", Value: strconv.Itoa(st.NumManagedBoxes)},
		{Key: "Zettel (total)", Value: strconv.Itoa(st.ZettelTotal)},
		{Key: "Zettel (indexed)", Value: strconv.Itoa(st.ZettelIndexed)},
		{Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")},
		{Key: "Duration last re-index", Value: fmt.Sprintf("%vms", st.DurLastReload.Milliseconds())},
		{Key: "Indexes since last re-index", Value: strconv.FormatUint(st.IndexesSinceReload, 10)},
		{Key: "Indexed words", Value: strconv.FormatUint(st.IndexedWords, 10)},
		{Key: "Indexed URLs", Value: strconv.FormatUint(st.IndexedUrls, 10)},
		{Key: "Zettel enrichments", Value: strconv.FormatUint(st.IndexUpdates, 10)},
	}
}

func (ps *boxService) DumpIndex(w io.Writer) {
	ps.manager.Dump(w)
}

func (ps *boxService) Refresh() error {
	ps.mxService.RLock()
	defer ps.mxService.RUnlock()
	if ps.manager != nil {
		return ps.manager.Refresh(context.Background())
	}
	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
42

43
44
45

46
47

48
49
50
51
52
53
54
55
56
57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24


25
26
27

28
29
30
31
32
33
34
35
36
37
38
39


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

56
57
58

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

-
+

-
+




+
+
+


-




+





+
-
-
+
+

-
+
+
+
+





+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+
+

-
+
-
-
+
-
-
-
+

-
+


-
+

-
+



-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
+
+


-
-
-
-
+
+
+
+
+

-
+


+
+
+
+
+


-
-
+
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+


+

-
-
+
+


-
+


-
+







-
+


-
-
+
+

-
+
+

-


-
+



-
-
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
-
-
-
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-


-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-

-
-
+
+
-
-
-
+
+

+
-
-
+
+
-
-
+
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+


-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
-
-
-

-
+
-
-
+
-
-

-
-
+
+
+
+

-
-
+

+
-
+
-
-
-
-
-
-
-
-
+
+
+

-
-
+
+


-
-
+
+




+
+
+
-
+
+
+
+
+
+
+

//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
	"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
}
	rtConfig  *myConfig
}

// Predefined Metadata keys for runtime configuration
// See: https://zettelstore.de/manual/h/00001004020000
const (
	keyDefaultCopyright  = "default-copyright"
	keyDefaultLicense    = "default-license"
	keyDefaultVisibility = "default-visibility"
	keyExpertMode        = "expert-mode"
	keyMaxTransclusions  = "max-transclusions"
	keySiteName          = "site-name"
	keyYAMLHeader        = "yaml-header"
	keyZettelFileSyntax  = "zettel-file-syntax"
)

var errUnknownVisibility = errors.New("unknown visibility")

func (cs *configService) Initialize() {
func (cs *configService) Initialize(logger *logger.Logger) {
	cs.logger = logger
	cs.descr = descriptionMap{
		meta.KeyDefaultCopyright: {"Default copyright", parseString, true},
		keyDefaultCopyright: {"Default copyright", parseString, true},
		meta.KeyDefaultLang:      {"Default language", parseString, true},
		meta.KeyDefaultRole:      {"Default role", parseString, true},
		keyDefaultLicense:   {"Default license", parseString, true},
		meta.KeyDefaultSyntax:    {"Default syntax", parseString, true},
		meta.KeyDefaultTitle:     {"Default title", parseString, true},
		meta.KeyDefaultVisibility: {
		keyDefaultVisibility: {
			"Default zettel visibility",
			func(val string) interface{} {
			func(val string) (any, error) {
				vis := meta.GetVisibility(val)
				if vis == meta.VisibilityUnknown {
					return nil
					return nil, errUnknownVisibility
				}
				return vis
				return vis, nil
			},
			true,
		},
		meta.KeyExpertMode: {"Expert mode", parseBool, true},
		meta.KeyFooterHTML: {"Footer HTML", parseString, true},
		meta.KeyHomeZettel: {"Home zettel", parseZid, true},
		meta.KeyListPageSize: {
			"List page size",
			func(val string) interface{} {
				iVal, err := strconv.Atoi(val)
				if err != nil {
					return nil
		keyExpertMode:          {"Expert mode", parseBool, true},
		config.KeyFooterZettel: {"Footer Zettel", parseInvalidZid, true},
		config.KeyHomeZettel:   {"Home zettel", parseZid, true},
		kernel.ConfigInsecureHTML: {
			"Insecure HTML",
			cs.noFrozen(func(val string) (any, error) {
				switch val {
				case kernel.ConfigSyntaxHTML:
					return config.SyntaxHTML, nil
				case kernel.ConfigMarkdownHTML:
					return config.MarkdownHTML, nil
				case kernel.ConfigZmkHTML:
					return config.ZettelmarkupHTML, nil
				}
				return iVal
			},
				return config.NoHTML, nil
			}),
			true,
		},
		meta.KeyMarkerExternal: {"Marker external URL", parseString, true},
		meta.KeySiteName:       {"Site name", parseString, true},
		meta.KeyYAMLHeader:     {"YAML header", parseBool, true},
		meta.KeyZettelFileSyntax: {
		api.KeyLang:         {"Language", parseString, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) interface{} { return strings.Fields(val) },
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},
		kernel.ConfigSimpleMode:        {"Simple mode", cs.noFrozen(parseBool), true},
		config.KeyShowBackLinks:        {"Show back links", parseString, true},
		config.KeyShowFolgeLinks:       {"Show folge links", parseString, true},
		config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true},
		config.KeyShowSuccessorLinks:   {"Show successor links", parseString, true},
	}
	cs.next = interfaceMap{
		meta.KeyDefaultCopyright:  "",
		meta.KeyDefaultLang:       meta.ValueLangEN,
		keyDefaultCopyright:            "",
		keyDefaultLicense:              "",
		meta.KeyDefaultRole:       meta.ValueRoleZettel,
		meta.KeyDefaultSyntax:     meta.ValueSyntaxZmk,
		meta.KeyDefaultTitle:      "Untitled",
		meta.KeyDefaultVisibility: meta.VisibilityLogin,
		meta.KeyExpertMode:        false,
		keyDefaultVisibility:           meta.VisibilityLogin,
		keyExpertMode:                  false,
		meta.KeyFooterHTML:        "",
		meta.KeyHomeZettel:        id.DefaultHomeZid,
		meta.KeyListPageSize:      0,
		meta.KeyMarkerExternal:    "&#10138;",
		meta.KeySiteName:          "Zettelstore",
		meta.KeyYAMLHeader:        false,
		meta.KeyZettelFileSyntax:  nil,
		config.KeyFooterZettel:         id.Invalid,
		config.KeyHomeZettel:           id.DefaultHomeZid,
		kernel.ConfigInsecureHTML:      config.NoHTML,
		api.KeyLang:                    api.ValueLangEN,
		keyMaxTransclusions:            int64(1024),
		keySiteName:                    "Zettelstore",
		keyYAMLHeader:                  false,
		keyZettelFileSyntax:            nil,
		kernel.ConfigSimpleMode:        false,
		config.KeyShowBackLinks:        "",
		config.KeyShowFolgeLinks:       "",
		config.KeyShowSubordinateLinks: "",
		config.KeyShowSuccessorLinks:   "",
	}
}
func (cs *configService) GetLogger() *logger.Logger { return cs.logger }

func (cs *configService) Start(kern *myKernel) error {
	kern.doLog("Start Config Service")
func (cs *configService) Start(*myKernel) error {
	cs.logger.Info().Msg("Start Service")
	data := meta.New(id.ConfigurationZid)
	for _, kv := range cs.GetNextConfigList() {
		data.Set(kv.Key, fmt.Sprintf("%v", kv.Value))
		data.Set(kv.Key, kv.Value)
	}
	cs.mxService.Lock()
	cs.rtConfig = newConfig(data)
	cs.orig = data
	cs.mxService.Unlock()
	return nil
}

func (cs *configService) IsStarted() bool {
	cs.mxService.RLock()
	defer cs.mxService.RUnlock()
	return cs.rtConfig != nil
	return cs.orig != nil
}

func (cs *configService) Stop(kern *myKernel) error {
	kern.doLog("Stop Config Service")
func (cs *configService) Stop(*myKernel) {
	cs.logger.Info().Msg("Stop Service")
	cs.mxService.Lock()
	cs.rtConfig = nil
	cs.orig = nil
	cs.manager = nil
	cs.mxService.Unlock()
	return nil
}

func (cs *configService) GetStatistics() []kernel.KeyValue {
func (*configService) GetStatistics() []kernel.KeyValue {
	return nil
}

func (cs *configService) setPlace(mgr place.Manager) {
	cs.rtConfig.setPlace(mgr)
func (cs *configService) setBox(mgr box.Manager) {
	cs.mxService.Lock()
	cs.manager = mgr
	cs.mxService.Unlock()
	mgr.RegisterObserver(cs.observe)
	cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
}

// myConfig contains all runtime configuration data relevant for the software.
type myConfig struct {
	mx   sync.RWMutex
	orig *meta.Meta
	data *meta.Meta
}

// New creates a new Config value.
func newConfig(orig *meta.Meta) *myConfig {
	cfg := myConfig{

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 {
		orig: orig,
		data: orig.Clone(),
	}
	return &cfg
}
func (cfg *myConfig) setPlace(mgr place.Manager) {
		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)
	mgr.RegisterObserver(cfg.observe)
	cfg.doUpdate(mgr)
}

		} else if defVal, defFound := cs.orig.Get(key); defFound {
			cs.SetConfig(key, defVal)
		}
	}
func (cfg *myConfig) doUpdate(p place.Place) error {
	m, err := p.GetMeta(context.Background(), cfg.data.Zid)
	if err != nil {
		return err
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}
	cfg.mx.Lock()
	for _, pair := range cfg.data.Pairs(false) {
		if val, ok := m.Get(pair.Key); ok {
			cfg.data.Set(pair.Key, val)
		}

	}
	cfg.mx.Unlock()
	return nil
func (cs *configService) observe(ci box.UpdateInfo) {
	if ci.Reason != box.OnZettel || 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)
func (cfg *myConfig) observe(ci place.UpdateInfo) {
	if ci.Reason == place.OnReload || ci.Zid == id.ConfigurationZid {
		go func() { cfg.doUpdate(ci.Place) }()
	}
}

			} else {
				cs.doUpdate(ci.Box)
			}
		}()
	}
var defaultKeys = map[string]string{
	meta.KeyCopyright: meta.KeyDefaultCopyright,
	meta.KeyLang:      meta.KeyDefaultLang,
	meta.KeyLicense:   meta.KeyDefaultLicense,
	meta.KeyRole:      meta.KeyDefaultRole,
	meta.KeySyntax:    meta.KeyDefaultSyntax,
	meta.KeyTitle:     meta.KeyDefaultTitle,
}

// AddDefaultValues enriches the given meta data with its default values.
func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta {
	if cfg == nil {
		return m
	}
// --- config.Config

func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
	if m != nil {
		if val, found := m.Get(key); found {
			return val
		}
	result := m
	cfg.mx.RLock()
	for k, d := range defaultKeys {
		if _, ok := result.Get(k); !ok {
			if result == m {
				result = m.Clone()
			}
			if val, ok := cfg.data.Get(d); ok {
				result.Set(k, val)
			}
		}
	}
	if user := server.GetUser(ctx); user != nil {
		if val, found := user.Get(key); found {
			return val
		}
	}
	}
	cfg.mx.RUnlock()
	return result
}

func (cfg *myConfig) getString(key string) string {
	result := cs.GetCurConfig(key)
	if result == nil {
		return ""
	}
	switch val := result.(type) {
	case string:
	cfg.mx.RLock()
	val, _ := cfg.data.Get(key)
	cfg.mx.RUnlock()
	return val
		return val
}
func (cfg *myConfig) getBool(key string) bool {
	case bool:
	cfg.mx.RLock()
	val := cfg.data.GetBool(key)
		if val {
	cfg.mx.RUnlock()
	return val
}

// GetDefaultTitle returns the current value of the "default-title" key.
func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(meta.KeyDefaultTitle) }

// GetDefaultRole returns the current value of the "default-role" key.
func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(meta.KeyDefaultRole) }

// GetDefaultSyntax returns the current value of the "default-syntax" key.
func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(meta.KeyDefaultSyntax) }

// GetDefaultLang returns the current value of the "default-lang" key.
			return api.ValueTrue
		}
		return api.ValueFalse
	case id.Zid:
		return val.String()
	case int:
		return strconv.Itoa(val)
	case []string:
		return strings.Join(val, " ")
	case meta.Visibility:
		return val.String()
	case fmt.Stringer:
		return val.String()
	case fmt.GoStringer:
		return val.GoString()
	}
	return fmt.Sprintf("%v", result)
func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(meta.KeyDefaultLang) }

}
// GetSiteName returns the current value of the "site-name" key.
func (cfg *myConfig) GetSiteName() string { return cfg.getString(meta.KeySiteName) }

// GetHomeZettel returns the value of the "home-zettel" key.
func (cfg *myConfig) GetHomeZettel() id.Zid {
// AddDefaultValues enriches the given meta data with its default values.
func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta {
	val := cfg.getString(meta.KeyHomeZettel)
	if homeZid, err := id.Parse(val); err == nil {
		return homeZid
	if cs == nil {
		return m
	}
	result := m
	cfg.mx.RLock()
	val, _ = cfg.orig.Get(meta.KeyHomeZettel)
	cs.mxService.RLock()
	if _, found := m.Get(api.KeyCopyright); !found {
	homeZid, _ := id.Parse(val)
	cfg.mx.RUnlock()
		result = updateMeta(result, m, api.KeyCopyright, cs.GetCurConfig(keyDefaultCopyright).(string))
	return homeZid
}

	}
	if _, found := m.Get(api.KeyLang); !found {
		result = updateMeta(result, m, api.KeyLang, cs.Get(ctx, nil, api.KeyLang))
	}
// GetDefaultVisibility returns the default value for zettel visibility.
func (cfg *myConfig) GetDefaultVisibility() meta.Visibility {
	val := cfg.getString(meta.KeyDefaultVisibility)
	if _, found := m.Get(api.KeyLicense); !found {
		result = updateMeta(result, m, api.KeyLicense, cs.GetCurConfig(keyDefaultLicense).(string))
	}
	if _, found := m.Get(api.KeyVisibility); !found {
		result = updateMeta(result, m, api.KeyVisibility, cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility).String())
	if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
		return vis
	}
	}
	cs.mxService.RUnlock()
	return result
}
	cfg.mx.RLock()
	val, _ = cfg.orig.Get(meta.KeyDefaultVisibility)
	vis := meta.GetVisibility(val)
	cfg.mx.RUnlock()
	return vis
func updateMeta(result, m *meta.Meta, key, val string) *meta.Meta {
	if result == m {
		result = m.Clone()
	}
	result.Set(key, val)
	return result
}

// GetYAMLHeader returns the current value of the "yaml-header" key.
func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(meta.KeyYAMLHeader) }

// GetMarkerExternal returns the current value of the "marker-external" key.
func (cs *configService) GetHTMLInsecurity() config.HTMLInsecurity {
	return cs.GetCurConfig(kernel.ConfigInsecureHTML).(config.HTMLInsecurity)
}

// GetSiteName returns the current value of the "site-name" key.
func (cfg *myConfig) GetMarkerExternal() string {
	return cfg.getString(meta.KeyMarkerExternal)
}

func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) }

// GetMaxTransclusions return the maximum number of indirect transclusions.
func (cs *configService) GetMaxTransclusions() int {
	return int(cs.GetCurConfig(keyMaxTransclusions).(int64))
}
// GetFooterHTML returns HTML code that should be embedded into the footer
// of each WebUI page.
func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(meta.KeyFooterHTML) }

// GetListPageSize returns the maximum length of a list to be returned in WebUI.
// GetYAMLHeader returns the current value of the "yaml-header" key.
// A value less or equal to zero signals no limit.
func (cfg *myConfig) GetListPageSize() int {
func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) }
	cfg.mx.RLock()
	defer cfg.mx.RUnlock()

	if value, ok := cfg.data.GetNumber(meta.KeyListPageSize); ok {
		return value
// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
func (cs *configService) GetZettelFileSyntax() []string {
	if zfs := cs.GetCurConfig(keyZettelFileSyntax); zfs != nil {
		return zfs.([]string)
	}
	value, _ := cfg.orig.GetNumber(meta.KeyListPageSize)
	return value
	return nil
}


// --- config.AuthConfig
// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
func (cfg *myConfig) GetZettelFileSyntax() []string {
	cfg.mx.RLock()
	defer cfg.mx.RUnlock()
	return cfg.data.GetListOrNil(meta.KeyZettelFileSyntax)
}

// --- AuthConfig

// GetSimpleMode returns true if system tuns in simple-mode.
func (cs *configService) GetSimpleMode() bool { return cs.GetCurConfig(kernel.ConfigSimpleMode).(bool) }

// GetExpertMode returns the current value of the "expert-mode" key
func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(meta.KeyExpertMode) }
// GetExpertMode returns the current value of the "expert-mode" key.
func (cs *configService) GetExpertMode() bool { return cs.GetCurConfig(keyExpertMode).(bool) }

// GetVisibility returns the visibility value, or "login" if none is given.
func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility {
	if val, ok := m.Get(meta.KeyVisibility); ok {
func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility {
	if val, ok := m.Get(api.KeyVisibility); ok {
		if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
			return vis
		}
	}

	vis := cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility)
	if vis != meta.VisibilityUnknown {
	return cfg.GetDefaultVisibility()
		return vis
	}
	cs.mxService.RLock()
	val, _ := cs.orig.Get(keyDefaultVisibility)
	vis = meta.GetVisibility(val)
	cs.mxService.RUnlock()
	return vis
}

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
33
34

-
+

-
+




+
+
+


-








+


+

+







//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"fmt"
	"io"
	"os"
	"runtime/metrics"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
)

type cmdSession struct {
	w        io.Writer
	kern     *myKernel
	echo     bool
62
63
64
65
66
67
68




69
70
71
72
73
74
75
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







+
+
+
+







		for _, arg := range args[1:] {
			io.WriteString(sess.w, " ")
			io.WriteString(sess.w, arg)
		}
	}
	sess.w.Write(sess.eol)
}

func (sess *cmdSession) usage(cmd, val string) {
	sess.println("Usage:", cmd, val)
}

func (sess *cmdSession) printTable(table [][]string) {
	maxLen := sess.calcMaxLen(table)
	if len(maxLen) == 0 {
		return
	}
	if sess.header {
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
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







+
-
-
+
+












+


+
+












-
-
+
+
-
-
-
-
-
-
-







				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},
	"env":         {"show environment values", cmdEnvironment},
	"get-config":  {"show current configuration data", cmdGetConfig},
	"header": {
		"toggle table header",
		func(sess *cmdSession, cmd string, args []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 },
	},
	"start": {"start service", cmdStart},
	"stat":  {"show service statistics", cmdStat},
	"stop":  {"stop service", cmdStop},
}

func cmdHelp(sess *cmdSession, cmd string, args []string) bool {
	cmds := make([]string, 0, len(commands))
func cmdHelp(sess *cmdSession, _ string, _ []string) bool {
	cmds := maps.Keys(commands)
	for key := range commands {
		if key == "" {
			continue
		}
		cmds = append(cmds, key)
	}
	sort.Strings(cmds)
	table := [][]string{{"Command", "Description"}}
	for _, cmd := range cmds {
		table = append(table, []string{cmd, commands[cmd].Text})
	}
	sess.printTable(table)
	return true
}
214
215
216
217
218
219
220
221

222
223

224
225
226

227
228
229
230
231
232
233
220
221
222
223
224
225
226

227
228

229
230
231

232
233
234
235
236
237
238
239







-
+

-
+


-
+







	table := [][]string{{"Key", "Description"}}
	for _, kd := range srv.ConfigDescriptions() {
		table = append(table, []string{kd.Key, kd.Descr})
	}
	sess.printTable(table)
	return true
}
func cmdGetConfig(sess *cmdSession, cmd string, args []string) bool {
func cmdGetConfig(sess *cmdSession, _ string, args []string) bool {
	showConfig(sess, args,
		listCurConfig, func(srv service, key string) interface{} { return srv.GetConfig(key) })
		listCurConfig, func(srv service, key string) interface{} { return srv.GetCurConfig(key) })
	return true
}
func cmdNextConfig(sess *cmdSession, cmd string, args []string) bool {
func cmdNextConfig(sess *cmdSession, _ string, args []string) bool {
	showConfig(sess, args,
		listNextConfig, func(srv service, key string) interface{} { return srv.GetNextConfig(key) })
	return true
}
func showConfig(sess *cmdSession, args []string,
	listConfig func(*cmdSession, service), getConfig func(service, string) interface{}) {

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
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







-
-
+
+
-














-
+















-
+


-
-
+
+
-


+

-
-
+
+
+
+




-
+
-
-
-
-
-
-

-
+







			srvD := sess.kern.srvs[kernel.Service(k)]
			sess.println("%% Service", srvD.name)
			listConfig(sess, srvD.srv)

		}
		return
	}
	srvD, ok := sess.kern.srvNames[args[0]]
	if !ok {
	srvD, found := getService(sess, args[0])
	if !found {
		sess.println("Unknown service:", args[0])
		return
	}
	if len(args) == 1 {
		listConfig(sess, srvD.srv)
		return
	}
	val := getConfig(srvD.srv, args[1])
	if val == nil {
		sess.println("Unknown key", args[1], "for service", args[0])
		return
	}
	sess.println(fmt.Sprintf("%v", val))
}
func listCurConfig(sess *cmdSession, srv service) {
	listConfig(sess, func() []kernel.KeyDescrValue { return srv.GetConfigList(true) })
	listConfig(sess, func() []kernel.KeyDescrValue { return srv.GetCurConfigList(true) })
}
func listNextConfig(sess *cmdSession, srv service) {
	listConfig(sess, srv.GetNextConfigList)
}
func listConfig(sess *cmdSession, getConfigList func() []kernel.KeyDescrValue) {
	l := getConfigList()
	table := [][]string{{"Key", "Value", "Description"}}
	for _, kdv := range l {
		table = append(table, []string{kdv.Key, kdv.Value, kdv.Descr})
	}
	sess.printTable(table)
}

func cmdSetConfig(sess *cmdSession, cmd string, args []string) bool {
	if len(args) < 3 {
		sess.println("Usage:", cmd, "SERIVCE KEY VALUE")
		sess.usage(cmd, "SERVICE KEY VALUE")
		return true
	}
	srvD, ok := sess.kern.srvNames[args[0]]
	if !ok {
	srvD, found := getService(sess, args[0])
	if !found {
		sess.println("Unknown service:", args[0])
		return true
	}
	key := args[1]
	newValue := strings.Join(args[2:], " ")
	if !srvD.srv.SetConfig(args[1], newValue) {
		sess.println("Unable to set key", args[1], "to value", newValue)
	if err := srvD.srv.SetConfig(key, newValue); err == nil {
		sess.kern.logger.Mandatory().Str("key", key).Str("value", newValue).Msg("Update system configuration")
	} else {
		sess.println("Unable to set key", args[1], "to value", newValue, "because:", err.Error())
	}
	return true
}

func cmdServices(sess *cmdSession, cmd string, args []string) bool {
func cmdServices(sess *cmdSession, _ string, _ []string) bool {
	names := make([]string, 0, len(sess.kern.srvNames))
	for name := range sess.kern.srvNames {
		names = append(names, name)
	}
	sort.Strings(names)

	table := [][]string{{"Service", "Status"}}
	for _, name := range names {
	for _, name := range sortedServiceNames(sess) {
		if sess.kern.srvNames[name].srv.IsStarted() {
			table = append(table, []string{name, "started"})
		} else {
			table = append(table, []string{name, "stopped"})
		}
	}
	sess.printTable(table)
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
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







-
+


-
+

-














+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
+


-
+

-





+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+







		sess.println(err.Error())
	}
	return true
}

func cmdStat(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.println("Usage:", cmd, "SERVICE")
		sess.usage(cmd, "SERVICE")
		return true
	}
	srvD, ok := sess.kern.srvNames[args[0]]
	srvD, ok := getService(sess, args[0])
	if !ok {
		sess.println("Unknown service", args[0])
		return true
	}
	kvl := srvD.srv.GetStatistics()
	if len(kvl) == 0 {
		return true
	}
	table := [][]string{{"Key", "Value"}}
	for _, kv := range kvl {
		table = append(table, []string{kv.Key, kv.Value})
	}
	sess.printTable(table)
	return true
}

func cmdLogLevel(sess *cmdSession, _ string, args []string) bool {
	kern := sess.kern
	if len(args) == 0 {
		// Write log levels
		level := kern.logger.Level()
		table := [][]string{
			{"Service", "Level", "Name"},
			{"kernel", strconv.Itoa(int(level)), level.String()},
		}
		for _, name := range sortedServiceNames(sess) {
			level = kern.srvNames[name].srv.GetLogger().Level()
			table = append(table, []string{name, strconv.Itoa(int(level)), level.String()})
		}
		sess.printTable(table)
		return true
	}
	var l *logger.Logger
	name := args[0]
	if name == "kernel" {
		l = kern.logger
	} else {
		srvD, ok := getService(sess, name)
		if !ok {
			return true
		}
		l = srvD.srv.GetLogger()
	}

	if len(args) == 1 {
		level := l.Level()
		sess.println(strconv.Itoa(int(level)), level.String())
		return true
	}

	level := args[1]
	uval, err := strconv.ParseUint(level, 10, 8)
	lv := logger.Level(uval)
	if err != nil || !lv.IsValid() {
		lv = logger.ParseLevel(level)
	}
	if !lv.IsValid() {
		sess.println("Invalid level:", level)
		return true
	}
	kern.logger.Mandatory().Str("name", name).Str("level", lv.String()).Msg("Update log level")
	l.SetLevel(lv)
	return true
}

func lookupService(sess *cmdSession, cmd string, args []string) (kernel.Service, bool) {
	if len(args) == 0 {
		sess.println("Usage:", cmd, "SERVICE")
		sess.usage(cmd, "SERVICE")
		return 0, false
	}
	srvD, ok := sess.kern.srvNames[args[0]]
	srvD, ok := getService(sess, args[0])
	if !ok {
		sess.println("Unknown service", args[0])
		return 0, false
	}
	return srvD.srvnum, true
}

func cmdProfile(sess *cmdSession, _ string, args []string) bool {
	var profileName string
	if len(args) < 1 {
		profileName = kernel.ProfileCPU
	} else {
		profileName = args[0]
	}
	var fileName string
	if len(args) < 2 {
		fileName = profileName + ".prof"
	} else {
		fileName = args[1]
	}
	kern := sess.kern
	if err := kern.doStartProfiling(profileName, fileName); err != nil {
		sess.println("Error:", err.Error())
	} else {
		kern.logger.Mandatory().Str("profile", profileName).Str("file", fileName).Msg("Start profiling")
	}
	return true
}
func cmdEndProfile(sess *cmdSession, _ string, _ []string) bool {
	kern := sess.kern
	err := kern.doStopProfiling()
	if err != nil {
		sess.println("Error:", err.Error())
	}
	kern.logger.Mandatory().Err(err).Msg("Stop profiling")
	return true
}

func cmdMetrics(sess *cmdSession, cmd string, args []string) bool {
func cmdMetrics(sess *cmdSession, _ string, _ []string) bool {
	var samples []metrics.Sample
	all := metrics.All()
	for _, d := range all {
		if d.Kind == metrics.KindFloat64Histogram {
			continue
		}
		samples = append(samples, metrics.Sample{Name: d.Name})
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
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







-
+















-
+



+
+
+
+
+
+
+
+


-
+













-
+







			descr = descr[:pos]
		}
		value := samples[i].Value
		i++
		var sVal string
		switch value.Kind() {
		case metrics.KindUint64:
			sVal = fmt.Sprintf("%v", value.Uint64())
			sVal = strconv.FormatUint(value.Uint64(), 10)
		case metrics.KindFloat64:
			sVal = fmt.Sprintf("%v", value.Float64())
		case metrics.KindFloat64Histogram:
			sVal = "(Histogramm)"
		case metrics.KindBad:
			sVal = "BAD"
		default:
			sVal = fmt.Sprintf("(unexpected metric kind: %v)", value.Kind())
		}
		table = append(table, []string{sVal, descr})
	}
	sess.printTable(table)
	return true
}

func cmdDumpIndex(sess *cmdSession, cmd string, args []string) bool {
func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool {
	sess.kern.DumpIndex(sess.w)
	return true
}

func cmdRefresh(sess *cmdSession, _ string, _ []string) bool {
	kern := sess.kern
	kern.logger.Mandatory().Msg("Refresh")
	kern.box.Refresh()
	return true
}

func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.println("Usage:", cmd, "RECOVER")
		sess.usage(cmd, "RECOVER")
		sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.")
		return true
	}
	lines := sess.kern.core.RecoverLines(args[0])
	if len(lines) == 0 {
		return true
	}
	for _, line := range lines {
		sess.println(line)
	}
	return true
}

func cmdEnvironment(sess *cmdSession, cmd string, args []string) bool {
func cmdEnvironment(sess *cmdSession, _ string, _ []string) bool {
	workDir, err := os.Getwd()
	if err != nil {
		workDir = err.Error()
	}
	execName, err := os.Executable()
	if err != nil {
		execName = err.Error()
469
470
471
472
473
474
475










556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572







+
+
+
+
+
+
+
+
+
+
		if pos := strings.IndexByte(env, '='); pos >= 0 && pos < len(env) {
			table = append(table, []string{env[:pos], env[pos+1:]})
		}
	}
	sess.printTable(table)
	return true
}

func sortedServiceNames(sess *cmdSession) []string { return maps.Keys(sess.kern.srvNames) }

func getService(sess *cmdSession, name string) (serviceData, bool) {
	srvD, found := sess.kern.srvNames[name]
	if !found {
		sess.println("Unknown service", name)
	}
	return srvD, found
}

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57

58
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24


25
26
27
28
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

-
+

-
+




+
+
+


-



+






+
-
-
+
+
+


-
+




















+










-
-
+
-
-
-











+
+

-
+

-
+





+
+
-
+






-
+

-

-
+






-
+






-
+


-
+

-
-
-
+
+
+


-
+



















-
+














-
-
+
+


-
+

-
+



-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-


















+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+













-
+

+
+
-
+

-
+



-
+

-
+


+
+
+
+
+
+
+
+
-
+

-
+
+
+

+
+
+
+
-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

type parseFunc func(string) interface{}
type parseFunc func(string) (any, error)
type configDescription struct {
	text    string
	parse   parseFunc
	canList bool
}
type descriptionMap map[string]configDescription
type interfaceMap map[string]interface{}

func (m interfaceMap) Clone() interfaceMap {
	if m == nil {
		return nil
	}
	result := make(interfaceMap, len(m))
	for k, v := range m {
		result[k] = v
	}
	return result
}

type srvConfig struct {
	logger   *logger.Logger
	mxConfig sync.RWMutex
	frozen   bool
	descr    descriptionMap
	cur      interfaceMap
	next     interfaceMap
}

func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription {
	cfg.mxConfig.RLock()
	defer cfg.mxConfig.RUnlock()
	keys := make([]string, 0, len(cfg.descr))
	for k := range cfg.descr {
	keys := maps.Keys(cfg.descr)
		keys = append(keys, k)
	}
	sort.Strings(keys)
	result := make([]serviceConfigDescription, 0, len(keys))
	for _, k := range keys {
		text := cfg.descr[k].text
		if strings.HasSuffix(k, "-") {
			text = text + " (list)"
		}
		result = append(result, serviceConfigDescription{Key: k, Descr: text})
	}
	return result
}

var errAlreadyFrozen = errors.New("value not allowed to be set")

func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc {
	return func(val string) interface{} {
	return func(val string) (any, error) {
		if cfg.frozen {
			return nil
			return nil, errAlreadyFrozen
		}
		return parse(val)
	}
}

var errListKeyNotFound = errors.New("no list key found")

func (cfg *srvConfig) SetConfig(key, value string) bool {
func (cfg *srvConfig) SetConfig(key, value string) error {
	cfg.mxConfig.Lock()
	defer cfg.mxConfig.Unlock()
	descr, ok := cfg.descr[key]
	if !ok {
		d, baseKey, num := cfg.getListDescription(key)
		if num < 0 {
			return false
			return errListKeyNotFound
		}
		format := baseKey + "%d"
		for i := num + 1; ; i++ {
			k := fmt.Sprintf(format, i)
			k := baseKey + strconv.Itoa(i)
			if _, ok = cfg.next[k]; !ok {
				break
			}
			delete(cfg.next, k)
		}
		if num == 0 {
			return true
			return nil
		}
		descr = d
	}
	parse := descr.parse
	if parse == nil {
		if cfg.frozen {
			return false
			return errAlreadyFrozen
		}
		cfg.next[key] = value
		return true
		return nil
	}
	iVal := parse(value)
	if iVal == nil {
		return false
	iVal, err := parse(value)
	if err != nil {
		return err
	}
	cfg.next[key] = iVal
	return true
	return nil
}

func (cfg *srvConfig) getListDescription(key string) (configDescription, string, int) {
	for k, d := range cfg.descr {
		if !strings.HasSuffix(k, "-") {
			continue
		}
		if !strings.HasPrefix(key, k) {
			continue
		}
		num, err := strconv.Atoi(key[len(k):])
		if err != nil || num < 0 {
			continue
		}
		return d, k, num
	}
	return configDescription{}, "", -1
}

func (cfg *srvConfig) GetConfig(key string) interface{} {
func (cfg *srvConfig) GetCurConfig(key string) interface{} {
	cfg.mxConfig.RLock()
	defer cfg.mxConfig.RUnlock()
	if cfg.cur == nil {
		return cfg.next[key]
	}
	return cfg.cur[key]
}

func (cfg *srvConfig) GetNextConfig(key string) interface{} {
	cfg.mxConfig.RLock()
	defer cfg.mxConfig.RUnlock()
	return cfg.next[key]
}

func (cfg *srvConfig) GetConfigList(all bool) []kernel.KeyDescrValue {
	return cfg.getConfigList(all, cfg.GetConfig)
func (cfg *srvConfig) GetCurConfigList(all bool) []kernel.KeyDescrValue {
	return cfg.getOneConfigList(all, cfg.GetCurConfig)
}
func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue {
	return cfg.getConfigList(true, cfg.GetNextConfig)
	return cfg.getOneConfigList(true, cfg.GetNextConfig)
}
func (cfg *srvConfig) getConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue {
func (cfg *srvConfig) getOneConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue {
	if len(cfg.descr) == 0 {
		return nil
	}
	keys := make([]string, 0, len(cfg.descr))
	keys := cfg.getSortedConfigKeys(all, getConfig)
	for k, descr := range cfg.descr {
		if all || descr.canList {
			if !strings.HasSuffix(k, "-") {
				keys = append(keys, k)
				continue
			}
			format := k + "%d"
			for i := 1; ; i++ {
				key := fmt.Sprintf(format, i)
				val := getConfig(key)
				if val == nil {
					break
				}
				keys = append(keys, key)
			}
		}
	}
	sort.Strings(keys)
	result := make([]kernel.KeyDescrValue, 0, len(keys))
	for _, k := range keys {
		val := getConfig(k)
		if val == nil {
			continue
		}
		descr, ok := cfg.descr[k]
		if !ok {
			descr, _, _ = cfg.getListDescription(k)
		}
		result = append(result, kernel.KeyDescrValue{
			Key:   k,
			Descr: descr.text,
			Value: fmt.Sprintf("%v", val),
		})
	}
	return result
}

func (cfg *srvConfig) getSortedConfigKeys(all bool, getConfig func(string) interface{}) []string {
	keys := make([]string, 0, len(cfg.descr))
	for k, descr := range cfg.descr {
		if all || descr.canList {
			if !strings.HasSuffix(k, "-") {
				keys = append(keys, k)
				continue
			}
			for i := 1; ; i++ {
				key := k + strconv.Itoa(i)
				val := getConfig(key)
				if val == nil {
					break
				}
				keys = append(keys, key)
			}
		}
	}
	sort.Strings(keys)
	return keys
}

func (cfg *srvConfig) Freeze() {
	cfg.mxConfig.Lock()
	cfg.frozen = true
	cfg.mxConfig.Unlock()
}

func (cfg *srvConfig) SwitchNextToCur() {
	cfg.mxConfig.Lock()
	defer cfg.mxConfig.Unlock()
	cfg.cur = cfg.next.Clone()
}

func parseString(val string) interface{} { return val }
func parseString(val string) (any, error) { return val, nil }

var errNoBoolean = errors.New("no boolean value")

func parseBool(val string) interface{} {
func parseBool(val string) (any, error) {
	if val == "" {
		return false
		return false, errNoBoolean
	}
	switch val[0] {
	case '0', 'f', 'F', 'n', 'N':
		return false
		return false, nil
	}
	return true
	return true, nil
}

func parseInt64(val string) (any, error) {
	if u64, err := strconv.ParseInt(val, 10, 64); err == nil {
		return u64, nil
	} else {
		return nil, err
	}
}

func parseZid(val string) interface{} {
func parseZid(val string) (any, error) {
	if zid, err := id.Parse(val); err == nil {
		return zid
		return zid, nil
	} else {
		return id.Invalid, err
	}
}

func parseInvalidZid(val string) (any, error) {
	zid, _ := id.Parse(val)
	return id.Invalid
	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
33
34
35
36
37
38
39
40
41


42
43

44
45
46
47
48
49
50

51
52
53

54
55

56
57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55

56
57
58

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) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"fmt"
	"net"
	"os"
	"runtime"
	"sort"
	"sync"
	"time"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type coreService struct {
	srvConfig
	started bool

	mxRecover  sync.RWMutex
	mapRecover map[string]recoverInfo
}
type recoverInfo struct {
	count uint64
	ts    time.Time
	info  interface{}
	stack []byte
}

func (cs *coreService) Initialize() {
func (cs *coreService) Initialize(logger *logger.Logger) {
	cs.logger = logger
	cs.mapRecover = make(map[string]recoverInfo)
	cs.descr = descriptionMap{
		kernel.CoreDebug:     {"Debug mode", parseBool, false},
		kernel.CoreGoArch:    {"Go processor architecture", nil, false},
		kernel.CoreGoOS:      {"Go Operating System", nil, false},
		kernel.CoreGoVersion: {"Go Version", nil, false},
		kernel.CoreHostname:  {"Host name", nil, false},
		kernel.CorePort: {
			"Port of command line server",
			cs.noFrozen(func(val string) interface{} {
			cs.noFrozen(func(val string) (any, error) {
				port, err := net.LookupPort("tcp", val)
				if err != nil {
					return nil
					return nil, err
				}
				return port
				return port, nil
			}),
			true,
		},
		kernel.CoreProgname: {"Program name", nil, false},
		kernel.CoreStarted:  {"Start time", nil, false},
		kernel.CoreVerbose:  {"Verbose output", parseBool, true},
		kernel.CoreVersion: {
			"Version",
			cs.noFrozen(func(val string) interface{} {
			cs.noFrozen(func(val string) (any, error) {
				if val == "" {
					return "unknown"
					return kernel.CoreDefaultVersion, nil
				}
				return val
				return val, nil
			}),
			false,
		},
		kernel.CoreVTime: {"Version time", nil, false},
	}
	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.TimestampLayout),
		kernel.CoreVerbose:   false,
	}
	if hn, err := os.Hostname(); err == nil {
		cs.next[kernel.CoreHostname] = hn
	}
}

func (cs *coreService) GetLogger() *logger.Logger { return cs.logger }

func (cs *coreService) Start(kern *myKernel) error {
func (cs *coreService) Start(*myKernel) error {
	cs.started = true
	return nil
}
func (cs *coreService) IsStarted() bool { return cs.started }
func (cs *coreService) Stop(*myKernel) error {
func (cs *coreService) Stop(*myKernel) {
	cs.started = false
	return nil
}

func (cs *coreService) GetStatistics() []kernel.KeyValue {
	cs.mxRecover.RLock()
	defer cs.mxRecover.RUnlock()
	names := make([]string, 0, len(cs.mapRecover))
	for n := range cs.mapRecover {
	names := maps.Keys(cs.mapRecover)
		names = append(names, n)
	}
	sort.Strings(names)
	result := make([]kernel.KeyValue, 0, 3*len(names))
	for _, n := range names {
		ri := cs.mapRecover[n]
		result = append(
			result,
			kernel.KeyValue{
				Key:   fmt.Sprintf("Recover %q / Count", n),
139
140
141
142
143
144
145
146

147
148
149
150
151
146
147
148
149
150
151
152

153
154
155
156
157
158







-
+





	)
}

func (cs *coreService) updateRecoverInfo(name string, recoverInfo interface{}, stack []byte) {
	cs.mxRecover.Lock()
	ri := cs.mapRecover[name]
	ri.count++
	ri.ts = time.Now()
	ri.ts = time.Now().Local()
	ri.info = recoverInfo
	ri.stack = stack
	cs.mapRecover[name] = ri
	cs.mxRecover.Unlock()
}

Changes to kernel/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



52
53
54
55
56
57
58





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
1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44

45
46
47
48
49
50
51





52
53
54
55
56
57
58
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

-
+

-
+




+
+
+






+


-



+

+

+


+


+
+




+
-
+



-
+
+
+
+
+

+
-
-
-
-
-
+
+
+
+
+








-
-
+
+
+







+
+
+
+
+
-
+

-
+


-
-
+
+
+

+
+


+

+
-
-
-
-
-
+
+
+
+
+




-
+


+
+
-
+


+
-
+


-
-
+
+










+
+
+
+
+
+
-
+


+
+
+







-
+

-
+



-
+

+
-
+

-
-
-
-
-
+
+
+
+
+

-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+


+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+




-
-
+
-
-
-
-
-
-






+











-
+



+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







-

-
+




+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


+
+
-
+





-
+






-
+








-
+



+








+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/signal"
	"runtime"
	"runtime/debug"
	"runtime/pprof"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

// myKernel is the main internal kernel.
type myKernel struct {
	logWriter *kernelLogWriter
	// started   bool
	logger    *logger.Logger
	wg        sync.WaitGroup
	mx        sync.RWMutex
	interrupt chan os.Signal
	debug     bool

	profileName string
	fileName    string
	profileFile *os.File
	profile     *pprof.Profile

	self kernelService
	core  coreService
	cfg   configService
	auth  authService
	place placeService
	web   webService
	core coreService
	cfg  configService
	auth authService
	box  boxService
	web  webService

	srvs     map[kernel.Service]serviceDescr
	srvNames map[string]serviceData
	depStart serviceDependency
	depStop  serviceDependency // reverse of depStart
}

type serviceDescr struct {
	srv  service
	name string
	srv      service
	name     string
	logLevel logger.Level
}
type serviceData struct {
	srv    service
	srvnum kernel.Service
}
type serviceDependency map[kernel.Service][]kernel.Service

const (
	defaultNormalLogLevel = logger.InfoLevel
	defaultSimpleLogLevel = logger.ErrorLevel
)

// create and start a new kernel.
// create a new kernel.
func init() {
	kernel.Main = createAndStart()
	kernel.Main = createKernel()
}

// create and start a new kernel.
func createAndStart() kernel.Kernel {
// create a new kernel.
func createKernel() kernel.Kernel {
	lw := newKernelLogWriter(8192)
	kern := &myKernel{
		logWriter: lw,
		logger:    logger.New(lw, "").SetLevel(defaultNormalLogLevel),
		interrupt: make(chan os.Signal, 5),
	}
	kern.self.kernel = kern
	kern.srvs = map[kernel.Service]serviceDescr{
		kernel.KernelService: {&kern.self, "kernel", defaultNormalLogLevel},
		kernel.CoreService:   {&kern.core, "core"},
		kernel.ConfigService: {&kern.cfg, "config"},
		kernel.AuthService:   {&kern.auth, "auth"},
		kernel.PlaceService:  {&kern.place, "place"},
		kernel.WebService:    {&kern.web, "web"},
		kernel.CoreService:   {&kern.core, "core", defaultNormalLogLevel},
		kernel.ConfigService: {&kern.cfg, "config", defaultNormalLogLevel},
		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 {
			panic(fmt.Sprintf("Key %q already given for service %v", key, srvD.name))
			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()
		srvD.srv.Initialize(l)
	}
	kern.depStart = serviceDependency{
		kernel.KernelService: nil,
		kernel.CoreService:   nil,
		kernel.CoreService:   {kernel.KernelService},
		kernel.ConfigService: {kernel.CoreService},
		kernel.AuthService:   {kernel.CoreService},
		kernel.PlaceService:  {kernel.CoreService, kernel.ConfigService, kernel.AuthService},
		kernel.WebService:    {kernel.ConfigService, kernel.AuthService, kernel.PlaceService},
		kernel.BoxService:    {kernel.CoreService, kernel.ConfigService, kernel.AuthService},
		kernel.WebService:    {kernel.ConfigService, kernel.AuthService, kernel.BoxService},
	}
	kern.depStop = make(serviceDependency, len(kern.depStart))
	for srv, deps := range kern.depStart {
		for _, dep := range deps {
			kern.depStop[dep] = append(kern.depStop[dep], srv)
		}
	}
	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.TimestampLayout))
}

func (kern *myKernel) Start(headline 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)
	signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM)
	go func() {
		// Wait for interrupt.
		sig := <-kern.interrupt
		if strSig := sig.String(); strSig != "" {
			kern.doLog("Shut down Zettelstore:", strSig)
			kern.logger.Info().Str("signal", strSig).Msg("Shut down Zettelstore")
		}
		kern.shutdown()
		kern.doShutdown()
		kern.wg.Done()
	}()

	kern.StartService(kernel.CoreService)
	kern.StartService(kernel.KernelService)
	if headline {
		logger := kern.logger
		kern.doLog(fmt.Sprintf(
		logger.Mandatory().Msg(fmt.Sprintf(
			"%v %v (%v@%v/%v)",
			kern.core.GetConfig(kernel.CoreProgname),
			kern.core.GetConfig(kernel.CoreVersion),
			kern.core.GetConfig(kernel.CoreGoVersion),
			kern.core.GetConfig(kernel.CoreGoOS),
			kern.core.GetConfig(kernel.CoreGoArch),
			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),
		))
		kern.doLog("Licensed under the latest version of the EUPL (European Union Public License)")
		if kern.auth.GetConfig(kernel.AuthReadonly).(bool) {
			kern.doLog("Read-only mode")
		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.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)
	if port > 0 {
		listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
		startLineServer(kern, listenAddr)
	}
}

func (kern *myKernel) shutdown() {
	kern.StopService(kernel.CoreService) // Will stop all other services.
		port := kern.core.GetNextConfig(kernel.CorePort).(int)
		if port > 0 {
			listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
			startLineServer(kern, listenAddr)
		}
	}
}

func (kern *myKernel) doShutdown() {
	kern.StopService(kernel.KernelService) // Will stop all other services.
}

func (kern *myKernel) WaitForShutdown() {
	kern.wg.Wait()
}

	kern.doStopProfiling()
func (kern *myKernel) SetDebug(enable bool) bool {
	kern.mx.Lock()
	prevDebug := kern.debug
	kern.debug = enable
	kern.mx.Unlock()
	return prevDebug
}

// --- Shutdown operation ----------------------------------------------------

// Shutdown the service. Waits for all concurrent activity to stop.
func (kern *myKernel) Shutdown(silent bool) {
	kern.logger.Trace().Msg("Shutdown")
	kern.interrupt <- &shutdownSignal{silent: silent}
}

type shutdownSignal struct{ silent bool }

func (s *shutdownSignal) String() string {
	if s.silent {
		return ""
	}
	return "shutdown"
}
func (s *shutdownSignal) Signal() { /* Just a signal */ }
func (*shutdownSignal) Signal() { /* Just a signal */ }

// --- Log operation ---------------------------------------------------------

func (kern *myKernel) GetKernelLogger() *logger.Logger {
	return kern.logger
}
// Log some activity.
func (kern *myKernel) Log(args ...interface{}) {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	kern.doLog(args...)
}
func (kern *myKernel) doLog(args ...interface{}) {
	log.Println(args...)

func (kern *myKernel) SetLogLevel(logLevel string) {
	defaultLevel, srvLevel := kern.parseLogLevel(logLevel)

	kern.mx.RLock()
	defer kern.mx.RUnlock()
	for srvN, srvD := range kern.srvs {
		if lvl, found := srvLevel[srvN]; found {
			srvD.srv.GetLogger().SetLevel(lvl)
		} else if defaultLevel != logger.NoLevel {
			srvD.srv.GetLogger().SetLevel(defaultLevel)
		}
	}
}

func (kern *myKernel) parseLogLevel(logLevel string) (logger.Level, map[kernel.Service]logger.Level) {
	defaultLevel := logger.NoLevel
	srvLevel := map[kernel.Service]logger.Level{}
	for _, spec := range strings.Split(logLevel, ";") {
		vals := cleanLogSpec(strings.Split(spec, ":"))
		switch len(vals) {
		case 0:
		case 1:
			if lvl := logger.ParseLevel(vals[0]); lvl.IsValid() {
				defaultLevel = lvl
			}
		default:
			serviceText, levelText := vals[0], vals[1]
			if srv, found := kern.srvNames[serviceText]; found {
				if lvl := logger.ParseLevel(levelText); lvl.IsValid() {
					srvLevel[srv.srvnum] = lvl
				}
			}
		}
	}
	return defaultLevel, srvLevel
}

func cleanLogSpec(rawVals []string) []string {
	vals := make([]string, 0, len(rawVals))
	for _, rVal := range rawVals {
		val := strings.TrimSpace(rVal)
		if val != "" {
			vals = append(vals, val)
		}
	}
	return vals
}

func (kern *myKernel) RetrieveLogEntries() []kernel.LogEntry {
	return kern.logWriter.retrieveLogEntries()
}

func (kern *myKernel) GetLastLogTime() time.Time {
	return kern.logWriter.getLastLogTime()
}

// 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 {
	kern.Log(name, "recovered from:", recoverInfo)
	stack := debug.Stack()
	os.Stderr.Write(stack)
	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")
var errProfileNotFound = errors.New("profile not found")

func (kern *myKernel) StartProfiling(profileName, fileName string) error {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	return kern.doStartProfiling(profileName, fileName)
}
func (kern *myKernel) doStartProfiling(profileName, fileName string) error {
	if kern.profileName != "" {
		return errProfileInWork
	}
	if profileName == kernel.ProfileCPU {
		f, err := os.Create(fileName)
		if err != nil {
			return err
		}
		err = pprof.StartCPUProfile(f)
		if err != nil {
			f.Close()
			return err
		}
		kern.profileName = profileName
		kern.fileName = fileName
		kern.profileFile = f
		return nil
	}
	profile := pprof.Lookup(profileName)
	if profile == nil {
		return errProfileNotFound
	}
	f, err := os.Create(fileName)
	if err != nil {
		return err
	}
	kern.profileName = profileName
	kern.fileName = fileName
	kern.profile = profile
	kern.profileFile = f
	runtime.GC() // get up-to-date statistics
	profile.WriteTo(f, 0)
	return nil
}

func (kern *myKernel) StopProfiling() error {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	return kern.doStopProfiling()
}
func (kern *myKernel) doStopProfiling() error {
	if kern.profileName == "" {
		return nil // No profile started
	}
	if kern.profileName == kernel.ProfileCPU {
		pprof.StopCPUProfile()
	}
	err := kern.profileFile.Close()
	kern.profileName = ""
	kern.fileName = ""
	kern.profile = nil
	kern.profileFile = nil
	return err
}

// --- Service handling --------------------------------------------------

var errUnknownService = errors.New("unknown service")

func (kern *myKernel) SetConfig(srvnum kernel.Service, key, value string) bool {
func (kern *myKernel) SetConfig(srvnum kernel.Service, key, value string) error {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.SetConfig(key, value)
	}
	return false
	return errUnknownService
}

func (kern *myKernel) GetConfig(srvnum kernel.Service, key string) interface{} {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetConfig(key)
		return srvD.srv.GetCurConfig(key)
	}
	return nil
}

func (kern *myKernel) GetConfigList(srvnum kernel.Service) []kernel.KeyDescrValue {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetConfigList(false)
		return srvD.srv.GetCurConfigList(false)
	}
	return nil
}

func (kern *myKernel) GetServiceStatistics(srvnum kernel.Service) []kernel.KeyValue {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetStatistics()
	}
	return nil
}

func (kern *myKernel) GetLogger(srvnum kernel.Service) *logger.Logger {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetLogger()
	}
	return kern.GetKernelLogger()
}

func (kern *myKernel) SetLevel(srvnum kernel.Service, level logger.Level) {
	if level.IsValid() {
		kern.mx.RLock()
		if srvD, ok := kern.srvs[srvnum]; ok {
			srvD.srv.GetLogger().SetLevel(level)
		}
		kern.mx.RUnlock()
	}
}

func (kern *myKernel) StartService(srvnum kernel.Service) error {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	return kern.doStartService(srvnum)
}
func (kern *myKernel) doStartService(srvnum kernel.Service) error {
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





















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







-
+
-
-












-
-
+
+


+


-
+
-
-

















-
-
+
+











+

-
+




-
+
+
+
+





-
+

-
-
+
+




-
-
+
+




















-
+






-
+



-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	return kern.doRestartService(srvnum)
}
func (kern *myKernel) doRestartService(srvnum kernel.Service) error {
	deps := kern.sortDependency(srvnum, kern.depStop, false)
	for _, srv := range deps {
		if err := srv.Stop(kern); err != nil {
		srv.Stop(kern)
			return err
		}
	}
	for i := len(deps) - 1; i >= 0; i-- {
		srv := deps[i]
		if err := srv.Start(kern); err != nil {
			return err
		}
		srv.SwitchNextToCur()
	}
	return nil
}

func (kern *myKernel) StopService(srvnum kernel.Service) error {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	kern.mx.Lock()
	defer kern.mx.Unlock()
	return kern.doStopService(srvnum)
}

func (kern *myKernel) doStopService(srvnum kernel.Service) error {
	for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) {
		if err := srv.Stop(kern); err != nil {
		srv.Stop(kern)
			return err
		}
	}
	return nil
}

func (kern *myKernel) sortDependency(
	srvnum kernel.Service,
	srvdeps serviceDependency,
	isStarted bool,
) []service {
	srvD, ok := kern.srvs[srvnum]
	if !ok {
		return nil
	}
	if srvD.srv.IsStarted() == isStarted {
		return nil
	}
	deps := srvdeps[srvnum]
	found := make(map[service]bool, 4)
	result := make([]service, 0, 4)
	found := make(map[service]bool, 8)
	result := make([]service, 0, len(found))
	for _, dep := range deps {
		srvDeps := kern.sortDependency(dep, srvdeps, isStarted)
		for _, depSrv := range srvDeps {
			if !found[depSrv] {
				result = append(result, depSrv)
				found[depSrv] = true
			}
		}
	}
	return append(result, srvD.srv)
}

func (kern *myKernel) DumpIndex(w io.Writer) {
	kern.place.DumpIndex(w)
	kern.box.DumpIndex(w)
}

type service interface {
	// Initialize the data for the service.
	Initialize()
	Initialize(*logger.Logger)

	// Get service logger.
	GetLogger() *logger.Logger

	// ConfigDescriptions returns a sorted list of configuration descriptions.
	ConfigDescriptions() []serviceConfigDescription

	// SetConfig stores a configuration value.
	SetConfig(key, value string) bool
	SetConfig(key, value string) error

	// GetConfig returns the current configuration value.
	GetConfig(key string) interface{}
	// GetCurConfig returns the current configuration value.
	GetCurConfig(key string) interface{}

	// GetNextConfig returns the next configuration value.
	GetNextConfig(key string) interface{}

	// GetConfigList returns a sorted list of current configuration data.
	GetConfigList(all bool) []kernel.KeyDescrValue
	// GetCurConfigList returns a sorted list of current configuration data.
	GetCurConfigList(all bool) []kernel.KeyDescrValue

	// GetNextConfigList returns a sorted list of next configuration data.
	GetNextConfigList() []kernel.KeyDescrValue

	// GetStatistics returns a key/value list of statistical data.
	GetStatistics() []kernel.KeyValue

	// Freeze disallows to change some fixed configuration values.
	Freeze()

	// Start the service.
	Start(*myKernel) error

	// SwitchNextToCur moves next config data to current.
	SwitchNextToCur()

	// IsStarted returns true if the service was started successfully.
	IsStarted() bool

	// Stop the service.
	Stop(*myKernel) error
	Stop(*myKernel)
}

type serviceConfigDescription struct{ Key, Descr string }

func (kern *myKernel) SetCreators(
	createAuthManager kernel.CreateAuthManagerFunc,
	createPlaceManager kernel.CreatePlaceManagerFunc,
	createBoxManager kernel.CreateBoxManagerFunc,
	setupWebServer kernel.SetupWebServerFunc,
) {
	kern.auth.createManager = createAuthManager
	kern.place.createManager = createPlaceManager
	kern.box.createManager = createBoxManager
	kern.web.setupServer = setupWebServer
}

// --- 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)                                   {}

Added kernel/impl/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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 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"
	"time"

	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
)

// kernelLogWriter adapts an io.Writer to a LogWriter
type kernelLogWriter struct {
	mx       sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries
	lastLog  time.Time
	buf      []byte
	writePos int
	data     []logEntry
	full     bool
}

// newKernelLogWriter creates a new LogWriter for kernel logging.
func newKernelLogWriter(capacity int) *kernelLogWriter {
	if capacity < 1 {
		capacity = 1
	}
	return &kernelLogWriter{
		lastLog: time.Now(),
		buf:     make([]byte, 0, 500),
		data:    make([]logEntry, capacity),
	}
}

func (klw *kernelLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error {
	klw.mx.Lock()

	if level > logger.DebugLevel {
		klw.lastLog = ts
		klw.data[klw.writePos] = logEntry{
			level:   level,
			ts:      ts,
			prefix:  prefix,
			msg:     msg,
			details: append([]byte(nil), details...),
		}
		klw.writePos++
		if klw.writePos >= cap(klw.data) {
			klw.writePos = 0
			klw.full = true
		}
	}

	klw.buf = klw.buf[:0]
	buf := klw.buf
	addTimestamp(&buf, ts)
	buf = append(buf, ' ')
	buf = append(buf, level.Format()...)
	buf = append(buf, ' ')
	if prefix != "" {
		buf = append(buf, prefix...)
		buf = append(buf, ' ')
	}
	buf = append(buf, msg...)
	buf = append(buf, details...)
	buf = append(buf, '\n')
	_, err := os.Stdout.Write(buf)

	klw.mx.Unlock()
	return err
}

func addTimestamp(buf *[]byte, ts time.Time) {
	year, month, day := ts.Date()
	itoa(buf, year, 4)
	*buf = append(*buf, '-')
	itoa(buf, int(month), 2)
	*buf = append(*buf, '-')
	itoa(buf, day, 2)
	*buf = append(*buf, ' ')
	hour, minute, second := ts.Clock()
	itoa(buf, hour, 2)
	*buf = append(*buf, ':')
	itoa(buf, minute, 2)
	*buf = append(*buf, ':')
	itoa(buf, second, 2)

}

func itoa(buf *[]byte, i, wid int) {
	var b [20]byte
	for bp := wid - 1; bp >= 0; bp-- {
		q := i / 10
		b[bp] = byte('0' + i - q*10)
		i = q
	}
	*buf = append(*buf, b[:wid]...)
}

type logEntry struct {
	level   logger.Level
	ts      time.Time
	prefix  string
	msg     string
	details []byte
}

func (klw *kernelLogWriter) retrieveLogEntries() []kernel.LogEntry {
	klw.mx.RLock()
	defer klw.mx.RUnlock()

	if !klw.full {
		if klw.writePos == 0 {
			return nil
		}
		result := make([]kernel.LogEntry, klw.writePos)
		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 := range klw.writePos {
		copyE2E(&result[pos], &klw.data[j])
		pos++
	}
	return result
}

func (klw *kernelLogWriter) getLastLogTime() time.Time {
	klw.mx.RLock()
	defer klw.mx.RUnlock()
	return klw.lastLog
}

func copyE2E(result *kernel.LogEntry, origin *logEntry) {
	result.Level = origin.level
	result.TS = origin.ts
	result.Prefix = origin.prefix
	result.Message = origin.msg + string(origin.details)
}

Deleted kernel/impl/place.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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


































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"sync"

	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
)

type placeService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       place.Manager
	createManager kernel.CreatePlaceManagerFunc
}

func (ps *placeService) Initialize() {
	ps.descr = descriptionMap{
		kernel.PlaceDefaultDirType: {
			"Default directory place type",
			ps.noFrozen(func(val string) interface{} {
				switch val {
				case kernel.PlaceDirTypeNotify, kernel.PlaceDirTypeSimple:
					return val
				}
				return nil
			}),
			true,
		},
		kernel.PlaceURIs: {
			"Place URI",
			func(val string) interface{} {
				uVal, err := url.Parse(val)
				if err != nil {
					return nil
				}
				if uVal.Scheme == "" {
					uVal.Scheme = "dir"
				}
				return uVal
			},
			true,
		},
	}
	ps.next = interfaceMap{
		kernel.PlaceDefaultDirType: kernel.PlaceDirTypeNotify,
	}
}

func (ps *placeService) Start(kern *myKernel) error {
	placeURIs := make([]*url.URL, 0, 4)
	format := kernel.PlaceURIs + "%d"
	for i := 1; ; i++ {
		u := ps.GetNextConfig(fmt.Sprintf(format, i))
		if u == nil {
			break
		}
		placeURIs = append(placeURIs, u.(*url.URL))
	}
	ps.mxService.Lock()
	defer ps.mxService.Unlock()
	mgr, err := ps.createManager(placeURIs, kern.auth.manager, kern.cfg.rtConfig)
	if err != nil {
		kern.doLog("Unable to create place manager:", err)
		return err
	}
	kern.doLog("Start Place Manager:", mgr.Location())
	if err := mgr.Start(context.Background()); err != nil {
		kern.doLog("Unable to start place manager:", err)
	}
	kern.cfg.setPlace(mgr)
	ps.manager = mgr
	return nil
}

func (ps *placeService) IsStarted() bool {
	ps.mxService.RLock()
	defer ps.mxService.RUnlock()
	return ps.manager != nil
}

func (ps *placeService) Stop(kern *myKernel) error {
	kern.doLog("Stop Place Manager")
	ps.mxService.RLock()
	mgr := ps.manager
	ps.mxService.RUnlock()
	err := mgr.Stop(context.Background())
	ps.mxService.Lock()
	ps.manager = nil
	ps.mxService.Unlock()
	return err
}

func (ps *placeService) GetStatistics() []kernel.KeyValue {
	var st place.Stats
	ps.mxService.RLock()
	ps.manager.ReadStats(&st)
	ps.mxService.RUnlock()
	return []kernel.KeyValue{
		{Key: "Read-only", Value: fmt.Sprintf("%v", st.ReadOnly)},
		{Key: "Sub-places", Value: fmt.Sprintf("%v", st.NumManagedPlaces)},
		{Key: "Zettel (total)", Value: fmt.Sprintf("%v", st.ZettelTotal)},
		{Key: "Zettel (indexed)", Value: fmt.Sprintf("%v", st.ZettelIndexed)},
		{Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")},
		{Key: "Indexes since last re-index", Value: fmt.Sprintf("%v", st.IndexesSinceReload)},
		{Key: "Duration last index", Value: fmt.Sprintf("%vms", st.DurLastIndex.Milliseconds())},
		{Key: "Indexed words", Value: fmt.Sprintf("%v", st.IndexedWords)},
		{Key: "Indexed URLs", Value: fmt.Sprintf("%v", st.IndexedUrls)},
		{Key: "Zettel enrichments", Value: fmt.Sprintf("%v", st.IndexUpdates)},
	}
}

func (ps *placeService) DumpIndex(w io.Writer) {
	ps.manager.Dump(w)
}

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
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
41
42
43
44

45
46
47
48
49
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) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"bufio"
	"net"
)

func startLineServer(kern *myKernel, listenAddr string) error {
	ln, err := net.Listen("tcp", listenAddr)
	if err != nil {
		kern.doLog("Unable to start Line Command Server:", err)
		kern.logger.Error().Err(err).Msg("Unable to start administration console")
		return err
	}
	kern.doLog("Start Line Command Server:", listenAddr)
	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.doLog("Unable to accept connection:", err)
			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)
	s := bufio.NewScanner(conn)
	for s.Scan() {
		line := s.Text()
		if !cmds.executeLine(line) {
			break
		}
	}
	conn.Close()
}

Changes to kernel/impl/web.go.

1
2

3
4

5
6
7
8



9
10
11
12
13
14

15




16

17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40


41
42


43






















44
45
46
47
48




49

50

51
52


53
54

55
56

57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40









41
42


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

-
+

-
+




+
+
+


-



+

+
+
+
+

+




+











-
-
-
-
-
-
-
-
-
+
+
-
-
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
-
+
+
+
+
-
+

+
-
-
+
+

-
+


+














-
+

-
+

-
+





+
+

+









-
+



-
+


-
+

-
+

-
+



+
+
+
+

+




+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+


-
+


-
-
+
+


-
+



+
+
+
+
+
+
+
+
+
+
+
+
+









-
-
-
+
+
+



-


-
+


//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the kernel implementation.
package impl

import (
	"errors"
	"net"
	"net/netip"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/web/server/impl"
)

type webService struct {
	srvConfig
	mxService   sync.RWMutex
	srvw        server.Server
	setupServer kernel.SetupWebServerFunc
}

// Constants for web service keys.
const (
	WebSecureCookie      = "secure"
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)
var errURLPrefixSyntax = errors.New("must not be empty and must start with '//'")


func (ws *webService) Initialize() {
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)
				if finfo, err := os.Stat(val); err == nil && finfo.IsDir() {
					return val, nil
				} else {
					return nil, err
				}
			},
			true,
		},
		kernel.WebBaseURL: {
			"Base URL",
			func(val string) (any, error) {
				if _, err := url.Parse(val); err != nil {
					return nil, err
				}
				return val, nil
			},
			true,
		},
		kernel.WebListenAddress: {
			"Listen address",
			func(val string) interface{} {
				host, port, err := net.SplitHostPort(val)
				if err != nil {
			func(val string) (any, error) {
				// If there is no host, prepend 127.0.0.1 as host.
				host, _, err := net.SplitHostPort(val)
				if err == nil && host == "" {
					return nil
					val = "127.0.0.1" + val
				}
				ap, err := netip.ParseAddrPort(val)
				if _, err := net.LookupPort("tcp", port); err != nil {
					return nil
				if err != nil {
					return "", err
				}
				return net.JoinHostPort(host, port)
				return ap.String(), nil
			},
			true},
		kernel.WebMaxRequestSize:   {"Max Request Size", parseInt64, true},
		kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true},
		kernel.WebSecureCookie:     {"Secure cookie", parseBool, true},
		kernel.WebTokenLifetimeAPI: {
			"Token lifetime API",
			makeDurationParser(10*time.Minute, 0, 1*time.Hour),
			true,
		},
		kernel.WebTokenLifetimeHTML: {
			"Token lifetime HTML",
			makeDurationParser(1*time.Hour, 1*time.Minute, 30*24*time.Hour),
			true,
		},
		kernel.WebURLPrefix: {
			"URL prefix under which the web server runs",
			func(val string) interface{} {
			func(val string) (any, error) {
				if val != "" && val[0] == '/' && val[len(val)-1] == '/' {
					return val
					return val, nil
				}
				return nil
				return nil, errURLPrefixSyntax
			},
			true,
		},
	}
	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.WebTokenLifetimeAPI:  1 * time.Hour,
		kernel.WebTokenLifetimeHTML: 10 * time.Minute,
		kernel.WebURLPrefix:         "/",
	}
}

func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc {
	return func(val string) interface{} {
	return func(val string) (any, error) {
		if d, err := strconv.ParseUint(val, 10, 64); err == nil {
			secs := time.Duration(d) * time.Minute
			if secs < minDur {
				return minDur
				return minDur, nil
			}
			if secs > maxDur {
				return maxDur
				return maxDur, nil
			}
			return secs
			return secs, nil
		}
		return defDur
		return defDur, nil
	}
}

var errWrongBasePrefix = errors.New(kernel.WebURLPrefix + " does not match " + kernel.WebBaseURL)

func (ws *webService) GetLogger() *logger.Logger { return ws.logger }

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)
	maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64)
	if maxRequestSize < 1024 {
		maxRequestSize = 1024

	srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager)
	err := kern.web.setupServer(srvw, kern.place.manager, kern.auth.manager, kern.cfg.rtConfig)
	}

	if !strings.HasSuffix(baseURL, urlPrefix) {
		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.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled")
	}

	srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg)
	if err != nil {
		kern.doLog("Unable to create Web Server:", err)
		ws.logger.Error().Err(err).Msg("Unable to create")
		return err
	}
	if kern.debug {
	if kern.core.GetNextConfig(kernel.CoreDebug).(bool) {
		srvw.SetDebug()
	}
	if err := srvw.Run(); err != nil {
		kern.doLog("Unable to start Web Service:", err)
	if err = srvw.Run(); err != nil {
		ws.logger.Error().Err(err).Msg("Unable to start")
		return err
	}
	kern.doLog("Start Web Service:", listenAddr)
	ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service")
	ws.mxService.Lock()
	ws.srvw = srvw
	ws.mxService.Unlock()

	if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) {
		listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
		if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
			ws.logger.Mandatory().Msg(strings.Repeat("--------------------", 3))
			ws.logger.Mandatory().Msg("Open your browser and enter the following URL:")
			ws.logger.Mandatory().Msg("    http://localhost" + listenAddr[idx:])
			ws.logger.Mandatory().Msg("")
			ws.logger.Mandatory().Msg("If this does not work, try:")
			ws.logger.Mandatory().Msg("    http://127.0.0.1" + listenAddr[idx:])
		}
	}

	return nil
}

func (ws *webService) IsStarted() bool {
	ws.mxService.RLock()
	defer ws.mxService.RUnlock()
	return ws.srvw != nil
}

func (ws *webService) Stop(kern *myKernel) error {
	kern.doLog("Stop Web Service")
	err := ws.srvw.Stop()
func (ws *webService) Stop(*myKernel) {
	ws.logger.Info().Msg("Stop Service")
	ws.srvw.Stop()
	ws.mxService.Lock()
	ws.srvw = nil
	ws.mxService.Unlock()
	return err
}

func (ws *webService) GetStatistics() []kernel.KeyValue {
func (*webService) GetStatistics() []kernel.KeyValue {
	return nil
}

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
42
43
44













45
46

47
48
49
50
51
52












53
54
55
56
57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24



25
26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41



42
43
44
45
46


47
48
49
50
51
52
53
54
55
56
57
58
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

-
+

-
+




+
+
+








+


+

-
-
-
+
+
+




+
+
+
+

-
+




-
-
-



+
+
-
-
+
+
+
+
+




+
+
+
+
+
+
+
+
+
+
+
+
+

-
+






+
+
+
+
+
+
+
+
+
+
+
+

















-
+










+
+
+
+
+
+






-
-
-
-
-
-
+
+
+
+
+
+
+




+






+


+
+
+
+
+
+
+
+
+
+
+
+








-
+

-
-
+
+


-
+

-
-
+
+
+
+
+
+
+
+
+
+




+
+


+











+
+
+
+
+
+
+
+




-
-
-
+
+
+


-
+




-
+



//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package 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/place"
	"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)
	Start(headline bool, lineServer bool, configFile string)

	// WaitForShutdown blocks the call until Shutdown is called.
	WaitForShutdown()

	// SetDebug to enable/disable debug mode
	SetDebug(enable bool) bool

	// Shutdown the service. Waits for all concurrent activities to stop.
	Shutdown(silent bool)

	// GetKernelLogger returns the kernel logger.
	GetKernelLogger() *logger.Logger
	// Log some activity.
	Log(args ...interface{})

	// SetLogLevel sets the logging level for logger maintained by the kernel.
	//
	// Its syntax is: (SERVICE ":")? LEVEL (";" (SERICE ":")? LEVEL)*.
	SetLogLevel(string)

	// LogRecover outputs some information about the previous panic.
	LogRecover(name string, recoverInfo interface{}) bool

	// StartProfiling starts profiling the software according to a profile.
	// It is an error to start more than one profile.
	//
	// profileName is a valid profile (see runtime/pprof/Lookup()), or the
	// value "cpu" for profiling the CPI.
	// fileName is the name of the file where the results are written to.
	StartProfiling(profileName, fileName string) error

	// StopProfiling stops the current profiling and writes the result to
	// the file, which was named during StartProfiling().
	// It will always be called before the software stops its operations.
	StopProfiling() error

	// SetConfig stores a configuration value.
	SetConfig(srv Service, key, value string) bool
	SetConfig(srv Service, key, value string) error

	// GetConfig returns a configuration value.
	GetConfig(srv Service, key string) interface{}

	// GetConfigList returns a sorted list of configuration data.
	GetConfigList(Service) []KeyDescrValue

	// GetLogger returns a logger for the given service.
	GetLogger(Service) *logger.Logger

	// SetLevel sets the logging level for the given service.
	SetLevel(Service, logger.Level)

	// RetrieveLogEntries returns all buffered log entries.
	RetrieveLogEntries() []LogEntry

	// GetLastLogTime returns the time when the last logging with level > DEBUG happened.
	GetLastLogTime() time.Time

	// 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

	// 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)

	// SetCreators store functions to be called when a service has to be created.
	SetCreators(CreateAuthManagerFunc, CreatePlaceManagerFunc, SetupWebServerFunc)
	SetCreators(CreateAuthManagerFunc, CreateBoxManagerFunc, SetupWebServerFunc)
}

// Main references the main kernel.
var Main Kernel

// Unit is a type with just one value.
type Unit struct{}

// ShutdownChan is a channel used to signal a system shutdown.
type ShutdownChan <-chan Unit

// Constants for profile names.
const (
	ProfileCPU  = "CPU"
	ProfileHead = "heap"
)

// Service specifies a service, e.g. web, ...
type Service uint8

// Constants for type Service.
const (
	_ Service = iota
	CoreService
	ConfigService
	AuthService
	PlaceService
	WebService
	_             Service = iota
	KernelService         // The Kernel itself is also a sevice
	CoreService           // Manages startup specific functionality
	ConfigService         // Provides access to runtime configuration
	AuthService           // Manages authentication
	BoxService            // Boxes provide zettel
	WebService            // Access to Zettelstore through Web-based API and WebUI
)

// Constants for core service system keys.
const (
	CoreDebug     = "debug"
	CoreGoArch    = "go-arch"
	CoreGoOS      = "go-os"
	CoreGoVersion = "go-version"
	CoreHostname  = "hostname"
	CorePort      = "port"
	CoreProgname  = "progname"
	CoreStarted   = "started"
	CoreVerbose   = "verbose"
	CoreVersion   = "version"
	CoreVTime     = "vtime"
)

// Defined values for core service.
const (
	CoreDefaultVersion = "unknown"
)

// Constants for config service keys.
const (
	ConfigSimpleMode   = "simple-mode"
	ConfigInsecureHTML = "insecure-html"
)

// Constants for authentication service keys.
const (
	AuthOwner    = "owner"
	AuthReadonly = "readonly"
)

// Constants for place service keys.
// Constants for box service keys.
const (
	PlaceDefaultDirType = "defdirtype"
	PlaceURIs           = "place-uri-"
	BoxDefaultDirType = "defdirtype"
	BoxURIs           = "box-uri-"
)

// Allowed values for PlaceDefaultDirType
// Allowed values for BoxDefaultDirType
const (
	PlaceDirTypeNotify = "notify"
	PlaceDirTypeSimple = "simple"
	BoxDirTypeNotify = "notify"
	BoxDirTypeSimple = "simple"
)

// Constants for config service keys.
const (
	ConfigSecureHTML   = "secure"
	ConfigSyntaxHTML   = "html"
	ConfigMarkdownHTML = "markdown"
	ConfigZmkHTML      = "zettelmarkup"
)

// Constants for web service keys.
const (
	WebAssetDir          = "asset-dir"
	WebBaseURL           = "base-url"
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"
	WebMaxRequestSize    = "max-request-size"
	WebSecureCookie      = "secure"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)

// KeyDescrValue is a triple of config data.
type KeyDescrValue struct{ Key, Descr, Value string }

// KeyValue is a pair of key and value.
type KeyValue struct{ Key, Value string }

// LogEntry stores values of one log line written by a logger.Logger
type LogEntry struct {
	Level   logger.Level
	TS      time.Time
	Prefix  string
	Message string
}

// CreateAuthManagerFunc is called to create a new auth manager.
type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error)

// CreatePlaceManagerFunc is called to create a new place manager.
type CreatePlaceManagerFunc func(
	placeURIs []*url.URL,
// CreateBoxManagerFunc is called to create a new box manager.
type CreateBoxManagerFunc func(
	boxURIs []*url.URL,
	authManager auth.Manager,
	rtConfig config.Config,
) (place.Manager, error)
) (box.Manager, error)

// SetupWebServerFunc is called to create a new web service handler.
type SetupWebServerFunc func(
	webServer server.Server,
	placeManager place.Manager,
	boxManager box.Manager,
	authManager auth.Manager,
	rtConfig config.Config,
) error

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/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
	InfoLevel                   // Log normal activities
	ErrorLevel                  // Log (persistent) errors
	MandatoryLevel              // Log only mandatory events
	NeverLevel                  // Logging is disabled
)

var logLevel = [...]string{
	"     ",
	"TRACE",
	"DEBUG",
	"INFO ",
	"ERROR",
	">>>>>",
	"NEVER",
}

var strLevel = [...]string{
	"",
	"trace",
	"debug",
	"info",
	"error",
	"mandatory",
	"disabled",
}

// IsValid returns true, if the level is a valid level
func (l Level) IsValid() bool { return TraceLevel <= l && l <= NeverLevel }

func (l Level) String() string {
	if l.IsValid() {
		return strLevel[l]
	}
	return strconv.Itoa(int(l))
}

// Format returns a string representation suitable for logging.
func (l Level) Format() string {
	if l.IsValid() {
		return logLevel[l]
	}
	return strconv.Itoa(int(l))
}

// ParseLevel returns the recognized level.
func ParseLevel(text string) Level {
	for lv := TraceLevel; lv <= NeverLevel; lv++ {
		if len(text) > 2 && strings.HasPrefix(strLevel[lv], text) {
			return lv
		}
	}
	return NoLevel
}

// Logger represents an objects that emits logging messages.
type Logger struct {
	lw        LogWriter
	levelVal  uint32
	prefix    string
	context   []byte
	topParent *Logger
	uProvider UserProvider
}

// LogWriter writes log messages to their specified destinations.
type LogWriter interface {
	WriteMessage(level Level, ts time.Time, prefix, msg string, details []byte) error
}

// New creates a new logger for the given service.
//
// This function must only be called from a kernel implementation, not from
// code that tries to log something.
func New(lw LogWriter, prefix string) *Logger {
	if prefix != "" && len(prefix) < 6 {
		prefix = (prefix + "     ")[:6]
	}
	result := &Logger{
		lw:        lw,
		levelVal:  uint32(InfoLevel),
		prefix:    prefix,
		context:   nil,
		uProvider: nil,
	}
	result.topParent = result
	return result
}

func newFromMessage(msg *Message) *Logger {
	if msg == nil {
		return nil
	}
	logger := msg.logger
	context := make([]byte, 0, len(msg.buf))
	context = append(context, msg.buf...)
	return &Logger{
		lw:        nil,
		levelVal:  0,
		prefix:    logger.prefix,
		context:   context,
		topParent: logger.topParent,
		uProvider: nil,
	}
}

// SetLevel sets the level of the logger.
func (l *Logger) SetLevel(newLevel Level) *Logger {
	if l != nil {
		if l.topParent != l {
			panic("try to set level for child logger")
		}
		atomic.StoreUint32(&l.levelVal, uint32(newLevel))
	}
	return l
}

// Level returns the current level of the given logger
func (l *Logger) Level() Level {
	if l != nil {
		return Level(atomic.LoadUint32(&l.levelVal))
	}
	return NeverLevel
}

// 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) }

// Info creates a message suitable for information data.
func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) }

// Error creates a message suitable for errors.
func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) }

// 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)
	if msg != nil {
		msg.level = NoLevel
	}
	return msg
}

// UserProvider allows to retrieve an user metadata from a context.
type UserProvider interface {
	GetUser(ctx context.Context) *meta.Meta
}

// WithUser creates a derivied logger that allows to retrieve and log user identifer.
func (l *Logger) WithUser(up UserProvider) *Logger {
	return &Logger{
		lw:        nil,
		levelVal:  0,
		prefix:    l.prefix,
		context:   l.context,
		topParent: l.topParent,
		uProvider: up,
	}
}

func (l *Logger) writeMessage(level Level, msg string, details []byte) error {
	return l.topParent.lw.WriteMessage(level, time.Now().Local(), l.prefix, msg, details)
}

Added logger/logger_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"testing"
	"time"

	"zettelstore.de/z/logger"
)

func TestParseLevel(t *testing.T) {
	testcases := []struct {
		text string
		exp  logger.Level
	}{
		{"tra", logger.TraceLevel},
		{"deb", logger.DebugLevel},
		{"info", logger.InfoLevel},
		{"err", logger.ErrorLevel},
		{"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 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 range b.N {
		log.Info().Str("key", "val").Msg("Benchmark")
	}
}

func BenchmarkMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "")
	for range b.N {
		log.Info().Msg("Benchmark")
	}
}

func BenchmarkCloneStrMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child()
	for range b.N {
		log.Info().Msg("123456789")
	}
}

Added 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/zettel/id"
)

// Message presents a message to log.
type Message struct {
	logger *Logger
	level  Level
	buf    []byte
}

func newMessage(logger *Logger, level Level) *Message {
	if logger != nil {
		if logger.topParent.Level() <= level {
			m := messagePool.Get().(*Message)
			m.logger = logger
			m.level = level
			m.buf = append(m.buf[:0], logger.context...)
			return m
		}
	}
	return nil
}

func recycleMessage(m *Message) {
	messagePool.Put(m)
}

var messagePool = &sync.Pool{
	New: func() interface{} {
		return &Message{
			buf: make([]byte, 0, 500),
		}
	},
}

// Enabled returns whether the message will log or not.
func (m *Message) Enabled() bool {
	return m != nil && m.level != NeverLevel
}

// Str adds a string value to the full message
func (m *Message) Str(text, val string) *Message {
	if m.Enabled() {
		buf := append(m.buf, ',', ' ')
		buf = append(buf, text...)
		buf = append(buf, '=')
		m.buf = append(buf, val...)
	}
	return m
}

// Bool adds a boolean value to the full message
func (m *Message) Bool(text string, val bool) *Message {
	if val {
		m.Str(text, "true")
	} else {
		m.Str(text, "false")
	}
	return m
}

// Bytes adds a byte slice value to the full message
func (m *Message) Bytes(text string, val []byte) *Message {
	if m.Enabled() {
		buf := append(m.buf, ',', ' ')
		buf = append(buf, text...)
		buf = append(buf, '=')
		m.buf = append(buf, val...)
	}
	return m
}

// Err adds an error value to the full message
func (m *Message) Err(err error) *Message {
	if err != nil {
		return m.Str("error", err.Error())
	}
	return m
}

// Int adds an integer to the full message
func (m *Message) Int(text string, i int64) *Message {
	return m.Str(text, strconv.FormatInt(i, 10))
}

// Uint adds an unsigned integer to the full message
func (m *Message) Uint(text string, u uint64) *Message {
	return m.Str(text, strconv.FormatUint(u, 10))
}

// User adds the user-id field of the given user to the message.
func (m *Message) User(ctx context.Context) *Message {
	if m.Enabled() {
		if up := m.logger.uProvider; up != nil {
			if user := up.GetUser(ctx); user != nil {
				m.buf = append(m.buf, ", user="...)
				if userID, found := user.Get(api.KeyUserID); found {
					m.buf = append(m.buf, userID...)
				} else {
					m.buf = append(m.buf, user.Zid.Bytes()...)
				}
			}
		}
	}
	return m
}

// HTTPIP adds the IP address of a HTTP request to the message.
func (m *Message) HTTPIP(r *http.Request) *Message {
	if r == nil {
		return m
	}
	if from := r.Header.Get("X-Forwarded-For"); from != "" {
		return m.Str("ip", from)
	}
	return m.Str("IP", r.RemoteAddr)
}

// Zid adds a zettel identifier to the full message
func (m *Message) Zid(zid id.Zid) *Message {
	return m.Bytes("zid", zid.Bytes())
}

// Msg add the given text to the message and writes it to the log.
func (m *Message) Msg(text string) {
	if m.Enabled() {
		m.logger.writeMessage(m.level, text, m.buf)
		recycleMessage(m)
	}
}

// Child creates a child logger with context of this message.
func (m *Message) Child() *Logger {
	return newFromMessage(m)
}

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
26







27
28









29
30
31
32







33
34
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
60
61
62
63
64
65
66


67




68
69
70


71
72
73

74



-
+

-
+




+
+
+






-
-
-
-
+
+
+
+




-
-
-
-
+
+
+
+
+
+
+


+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+


-
-
-
-
+
+
+
+
+
+
+







-
-
+
-
-
-
-
+
+
+
-
-
+


-
+
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package 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"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:         "gif",
		AltNames:     nil,
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxGif,
		AltNames:      nil,
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.SyntaxJPEG,
		AltNames:      []string{meta.SyntaxJPG},
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:         "jpeg",
		AltNames:     []string{"jpg"},
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxPNG,
		AltNames:      nil,
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:         "png",
		AltNames:     nil,
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxWebp,
		AltNames:      nil,
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
	if p := parser.Get(syntax); p != nil {
		syntax = p.Name
	}
	title, _ := m.Get(meta.KeyTitle)
	return ast.BlockSlice{
	return ast.BlockSlice{&ast.BLOBNode{
		&ast.BLOBNode{
			Title:  title,
			Syntax: syntax,
			Blob:   []byte(inp.Src),
		Description: parser.ParseDescription(m),
		Syntax:      syntax,
		Blob:        []byte(inp.Src),
		},
	}
	}}
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
func parseInlines(*input.Input, string) ast.InlineSlice { return nil }
	return ast.InlineSlice{}
}

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
24
25
26
27












28
29

30
31
32
















33
34
35
36
37










38
39
40
41
42
43






44
45
46

47
48
49
50
51
52


53
54
55

56
57

58
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









































1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21

22
23
24
25





26
27
28
29
30
31
32
33
34
35
36
37
38

39

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57





58
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package cleaner provides funxtions to clean up the parsed AST.
// Package cleaner provides functions to clean up the parsed AST.
package cleaner

import (
	"strconv"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
)

// CleanupBlockSlice cleans the given block slice.
func CleanupBlockSlice(bs ast.BlockSlice) {
	cv := &cleanupVisitor{
		textEnc: encoder.Create("text", nil),
		doMark:  false,
// CleanBlockSlice cleans the given block list.
func CleanBlockSlice(bs *ast.BlockSlice, allowHTML bool) { cleanNode(bs, allowHTML) }

// CleanInlineSlice cleans the given inline list.
func CleanInlineSlice(is *ast.InlineSlice) { cleanNode(is, false) }

func cleanNode(n ast.Node, allowHTML bool) {
	cv := cleanVisitor{
		textEnc:   textenc.Create(),
		allowHTML: allowHTML,
		hasMark:   false,
		doMark:    false,
	}
	t := ast.NewTopDownTraverser(cv)
	ast.Walk(&cv, n)
	t.VisitBlockSlice(bs)
	if cv.hasMark {
		cv.doMark = true
		ast.Walk(&cv, n)
	}
}

type cleanVisitor struct {
	textEnc   *textenc.Encoder
	ids       map[string]ast.Node
	allowHTML bool
	hasMark   bool
	doMark    bool
}

func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		if !cv.allowHTML {
		t.VisitBlockSlice(bs)
	}
}

type cleanupVisitor struct {
			cv.visitBlockSlice(n)
			return nil
		}
	case *ast.InlineSlice:
		if !cv.allowHTML {
			cv.visitInlineSlice(n)
			return nil
		}
	case *ast.HeadingNode:
		cv.visitHeading(n)
	textEnc encoder.Encoder
	ids     map[string]ast.Node
	hasMark bool
	doMark  bool
}

		return nil
	case *ast.MarkNode:
		cv.visitMark(n)
		return nil
	}
	return cv
// VisitVerbatim does nothing.
func (cv *cleanupVisitor) VisitVerbatim(vn *ast.VerbatimNode) {}

}
// VisitRegion does nothing.
func (cv *cleanupVisitor) VisitRegion(rn *ast.RegionNode) {}

// VisitHeading calculates the heading slug.
func (cv *cleanupVisitor) VisitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || hn.Inlines == nil {
func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) {
	if bs == nil {
		return
	}
	var sb strings.Builder
	if len(*bs) == 0 {
	_, err := cv.textEnc.WriteInlines(&sb, hn.Inlines)
	if err != nil {
		*bs = nil
		return
	}
	s := strfun.Slugify(sb.String())
	for _, bn := range *bs {
	if len(s) > 0 {
		hn.Slug = cv.addIdentifier(s, hn)
		ast.Walk(cv, bn)
	}
}


	fromPos, toPos := 0, 0
// VisitHRule does nothing.
func (cv *cleanupVisitor) VisitHRule(hn *ast.HRuleNode) {}

	for fromPos < len(*bs) {
		(*bs)[toPos] = (*bs)[fromPos]
		fromPos++
		switch bn := (*bs)[toPos].(type) {
		case *ast.VerbatimNode:
			if bn.Kind != ast.VerbatimHTML {
				toPos++
			}
// VisitList does nothing.
func (cv *cleanupVisitor) VisitNestedList(ln *ast.NestedListNode) {}

		default:
			toPos++
		}
// VisitDescriptionList does nothing.
func (cv *cleanupVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {}

	}
// VisitPara does nothing.
func (cv *cleanupVisitor) VisitPara(pn *ast.ParaNode) {}

	for pos := toPos; pos < len(*bs); pos++ {
		(*bs)[pos] = nil // Allow excess nodes to be garbage collected.
	}
// VisitTable does nothing.
func (cv *cleanupVisitor) VisitTable(tn *ast.TableNode) {}

// VisitBLOB does nothing.
func (cv *cleanupVisitor) VisitBLOB(bn *ast.BLOBNode) {}

// VisitText does nothing.
func (cv *cleanupVisitor) VisitText(tn *ast.TextNode) {}

// VisitTag does nothing.
	*bs = (*bs)[:toPos:toPos]
}

func (cv *cleanVisitor) visitInlineSlice(is *ast.InlineSlice) {
	if is == nil {
		return
	}
	if len(*is) == 0 {
		*is = nil
		return
	}
	for _, bn := range *is {
		ast.Walk(cv, bn)
	}

func (cv *cleanupVisitor) VisitTag(tn *ast.TagNode) {}

	fromPos, toPos := 0, 0
	for fromPos < len(*is) {
		(*is)[toPos] = (*is)[fromPos]
		fromPos++
		switch in := (*is)[toPos].(type) {
		case *ast.LiteralNode:
			if in.Kind != ast.LiteralHTML {
				toPos++
			}
// VisitSpace does nothing.
func (cv *cleanupVisitor) VisitSpace(sn *ast.SpaceNode) {}

		default:
			toPos++
		}
// VisitBreak does nothing.
func (cv *cleanupVisitor) VisitBreak(bn *ast.BreakNode) {}

	}
// VisitLink collects the given link as a reference.
func (cv *cleanupVisitor) VisitLink(ln *ast.LinkNode) {}

	for pos := toPos; pos < len(*is); pos++ {
		(*is)[pos] = nil // Allow excess nodes to be garbage collected.
	}
// VisitImage collects the image links as a reference.
func (cv *cleanupVisitor) VisitImage(in *ast.ImageNode) {}

	*is = (*is)[:toPos:toPos]
}
// VisitCite does nothing.
func (cv *cleanupVisitor) VisitCite(cn *ast.CiteNode) {}

// VisitFootnote does nothing.
func (cv *cleanupVisitor) VisitFootnote(fn *ast.FootnoteNode) {}

// VisitMark checks for duplicate marks and changes them.
func (cv *cleanupVisitor) VisitMark(mn *ast.MarkNode) {
	if mn == nil {
		return
	}
func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
		return
	}
	if hn.Slug == "" {
		var sb strings.Builder
		_, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
		if err != nil {
			return
		}
		hn.Slug = strfun.Slugify(sb.String())
	}
	if hn.Slug != "" {
		hn.Fragment = cv.addIdentifier(hn.Slug, hn)
	}
}

func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) {
	if !cv.doMark {
		cv.hasMark = true
		return
	}
	if mn.Text == "" {
		mn.Text = cv.addIdentifier("*", mn)
	if mn.Mark == "" {
		mn.Slug = ""
		mn.Fragment = cv.addIdentifier("*", mn)
		return
	}
	mn.Text = cv.addIdentifier(mn.Text, mn)
}

	if mn.Slug == "" {
		mn.Slug = strfun.Slugify(mn.Mark)
	}
	mn.Fragment = cv.addIdentifier(mn.Slug, mn)
// VisitFormat does nothing.
func (cv *cleanupVisitor) VisitFormat(fn *ast.FormatNode) {}

}
// VisitLiteral does nothing.
func (cv *cleanupVisitor) VisitLiteral(ln *ast.LiteralNode) {}

func (cv *cleanupVisitor) addIdentifier(id string, node ast.Node) string {
func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string {
	if cv.ids == nil {
		cv.ids = map[string]ast.Node{id: node}
		return id
	}
	if n, ok := cv.ids[id]; ok && n != node {
		prefix := id + "-"
		for count := 1; ; count++ {
			newID := prefix + strconv.Itoa(count)
			if n, ok := cv.ids[newID]; !ok || n == node {
			if n2, ok2 := cv.ids[newID]; !ok2 || n2 == node {
				cv.ids[newID] = node
				return newID
			}
		}
	}
	cv.ids[id] = node
	return id
}

// CleanInlineLinks removes all links and footnote node from the given inline slice.
func CleanInlineLinks(is *ast.InlineSlice) { ast.Walk(&cleanLinks{}, is) }

type cleanLinks struct{}

func (cl *cleanLinks) Visit(node ast.Node) ast.Visitor {
	ins, ok := node.(*ast.InlineSlice)
	if !ok {
		return cl
	}
	for _, in := range *ins {
		ast.Walk(cl, in)
	}
	if hasNoLinks(*ins) {
		return nil
	}

	result := make(ast.InlineSlice, 0, len(*ins))
	for _, in := range *ins {
		switch n := in.(type) {
		case *ast.LinkNode:
			result = append(result, n.Inlines...)
		case *ast.FootnoteNode: // Do nothing
		default:
			result = append(result, n)
		}
	}
	*ins = result
	return nil
}

func hasNoLinks(ins ast.InlineSlice) bool {
	for _, in := range ins {
		switch in.(type) {
		case *ast.LinkNode, *ast.FootnoteNode:
			return false
		}
	}
	return true
}

Added parser/draw/ORIG_CONTRIBUTORS.




1
2
3
+
+
+
Devon H. O'Dell <devon.odell@gmail.com>
Marc-Antoine Ruel <maruel@gmail.com>
Mateusz Czaplinski <czapkofan@gmail.com>

Added parser/draw/ORIG_LICENSE.























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The MIT License (MIT)

Copyright (c) 2015 The ASCIIToSVG Contributors

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.

Added 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"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) {
	c := &canvas{}

	lines := bytes.Split(data, []byte("\n"))
	c.siz.Y = len(lines)

	// Diagrams will often not be padded to a uniform width. To overcome this, we scan over
	// each line and figure out which is the longest. This becomes the width of the canvas.
	for i, line := range lines {
		if ok := utf8.Valid(line); !ok {
			return nil, fmt.Errorf("invalid UTF-8 encoding on line %d", i)
		}
		if i1 := utf8.RuneCount(line); i1 > c.siz.X {
			c.siz.X = i1
		}
	}

	c.grid = make([]char, c.siz.X*c.siz.Y)
	c.visited = make([]bool, c.siz.X*c.siz.Y)
	for y, line := range lines {
		x := 0
		for len(line) > 0 {
			r, l := utf8.DecodeRune(line)
			c.grid[y*c.siz.X+x] = char(r)
			x++
			line = line[l:]
		}

		for ; x < c.siz.X; x++ {
			c.grid[y*c.siz.X+x] = ' '
		}
	}

	c.findObjects()
	return c, nil
}

// canvas is the parsed source data.
type canvas struct {
	// (0,0) is top left.
	grid           []char
	visited        []bool
	objs           objects
	siz            image.Point
	hasStartMarker bool
	hasEndMarker   bool
}

// String provides a view into the underlying grid.
func (c *canvas) String() string { return fmt.Sprintf("%+v", c.grid) }

// objects returns all the objects found in the underlying grid.
func (c *canvas) objects() objects { return c.objs }

// 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)
}

// 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 := range c.siz.Y {
		p := point{y: y}
		for x := range c.siz.X {
			p.x = x
			if c.isVisited(p) {
				continue
			}
			ch := c.at(p)
			if !ch.isPathStart() {
				continue
			}

			// Found the start of a one or multiple connected paths. Traverse all
			// connecting points. This will generate multiple objects if multiple
			// paths (either open or closed) are found.
			c.visit(p)
			objs := c.scanPath([]point{p})
			for _, obj := range objs {
				// For all points in all objects found, mark the points as visited.
				for _, p := range obj.Points() {
					c.visit(p)
				}
			}
			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 := range c.siz.Y {
		p := point{}
		p.y = y
		for x := range c.siz.X {
			p.x = x
			if c.isVisited(p) {
				continue
			}
			ch := c.at(p)
			if !ch.isTextStart() {
				continue
			}

			// scanText will return nil if the text at this area is simply
			// setting options on a container object.
			obj := c.scanText(p)
			if obj == nil {
				continue
			}
			for _, p := range obj.Points() {
				c.visit(p)
			}
			c.objs = append(c.objs, obj)
		}
	}
}

// scanPath tries to complete a total path (for lines or polygons) starting with some partial path.
// It recurses when it finds multiple unvisited outgoing paths.
func (c *canvas) scanPath(points []point) objects {
	cur := points[len(points)-1]
	next := c.next(cur)

	// If there are no points that can progress traversal of the path, finalize the one we're
	// working on, and return it. This is the terminal condition in the passive flow.
	if len(next) == 0 {
		if len(points) == 1 {
			// Discard 'path' of 1 point. Do not mark point as visited.
			c.unvisit(cur)
			return nil
		}

		// TODO(dhobsd): Determine if path is sharing the line with another path. If so,
		// we may want to join the objects such that we don't get weird rendering artifacts.
		o := &object{points: points}
		o.seal(c)
		return objects{o}
	}

	// If we have hit a point that can create a closed path, create an object and close
	// the path. Additionally, recurse to other progress directions in case e.g. an open
	// path spawns from this point. Paths are always closed vertically.
	if cur.x == points[0].x && cur.y == points[0].y+1 {
		o := &object{points: points}
		o.seal(c)
		r := objects{o}
		return append(r, c.scanPath([]point{cur})...)
	}

	// We scan depth-first instead of breadth-first, making it possible to find a
	// closed path.
	var objs objects
	for _, n := range next {
		if c.isVisited(n) {
			continue
		}
		c.visit(n)
		p2 := make([]point, len(points)+1)
		copy(p2, points)
		p2[len(p2)-1] = n
		objs = append(objs, c.scanPath(p2)...)
	}
	return objs
}

// The next returns the points that can be used to make progress, scanning (in order) horizontal
// progress to the left or right, vertical progress above or below, or diagonal progress to NW,
// NE, SW, and SE. It skips any points already visited, and returns all of the possible progress
// points.
func (c *canvas) next(pos point) []point {
	// Our caller must have called c.visit prior to calling this function.
	if !c.isVisited(pos) {
		panic(fmt.Errorf("internal error; revisiting %s", pos))
	}

	var out []point

	nextHorizontal := func(p point) {
		if !c.isVisited(p) && c.at(p).canHorizontal() {
			out = append(out, p)
		}
	}
	nextVertical := func(p point) {
		if !c.isVisited(p) && c.at(p).canVertical() {
			out = append(out, p)
		}
	}
	nextDiagonal := func(from, to point) {
		if !c.isVisited(to) && c.at(to).canDiagonalFrom(c.at(from)) {
			out = append(out, to)
		}
	}

	ch := c.at(pos)
	if ch.canHorizontal() {
		if c.canLeft(pos) {
			n := pos
			n.x--
			nextHorizontal(n)
		}
		if c.canRight(pos) {
			n := pos
			n.x++
			nextHorizontal(n)
		}
	}
	if ch.canVertical() {
		if c.canUp(pos) {
			n := pos
			n.y--
			nextVertical(n)
		}
		if c.canDown(pos) {
			n := pos
			n.y++
			nextVertical(n)
		}
	}
	if c.canDiagonal(pos) {
		if c.canUp(pos) {
			if c.canLeft(pos) {
				n := pos
				n.x--
				n.y--
				nextDiagonal(pos, n)
			}
			if c.canRight(pos) {
				n := pos
				n.x++
				n.y--
				nextDiagonal(pos, n)
			}
		}
		if c.canDown(pos) {
			if c.canLeft(pos) {
				n := pos
				n.x--
				n.y++
				nextDiagonal(pos, n)
			}
			if c.canRight(pos) {
				n := pos
				n.x++
				n.y++
				nextDiagonal(pos, n)
			}
		}
	}

	return out
}

// scanText extracts a line of text.
func (c *canvas) scanText(start point) *object {
	obj := &object{points: []point{start}, isText: true}
	whiteSpaceStreak := 0
	cur := start

	for c.canRight(cur) {
		cur.x++
		if c.isVisited(cur) {
			// If the point is already visited, we hit a polygon or a line.
			break
		}
		ch := c.at(cur)
		if !ch.isTextCont() {
			break
		}
		if ch.isSpace() {
			whiteSpaceStreak++
			// Stop when we see 3 consecutive whitespace points.
			if whiteSpaceStreak > 2 {
				break
			}
		} else {
			whiteSpaceStreak = 0
		}
		obj.points = append(obj.points, cur)
	}

	// Trim the right side of the text object.
	for len(obj.points) != 0 && c.at(obj.points[len(obj.points)-1]).isSpace() {
		obj.points = obj.points[:len(obj.points)-1]
	}

	obj.seal(c)
	return obj
}

func (c *canvas) at(p point) char {
	return c.grid[p.y*c.siz.X+p.x]
}

func (c *canvas) isVisited(p point) bool {
	return c.visited[p.y*c.siz.X+p.x]
}

func (c *canvas) visit(p point) {
	// TODO(dhobsd): Change code to ensure that visit() is called once and only
	// once per point.
	c.visited[p.y*c.siz.X+p.x] = true
}

func (c *canvas) unvisit(p point) {
	o := p.y*c.siz.X + p.x
	if !c.visited[o] {
		panic(fmt.Errorf("internal error: point %+v never visited", p))
	}
	c.visited[o] = false
}

func (*canvas) canLeft(p point) bool    { return p.x > 0 }
func (c *canvas) canRight(p point) bool { return p.x < c.siz.X-1 }
func (*canvas) canUp(p point) bool      { return p.y > 0 }
func (c *canvas) canDown(p point) bool  { return p.y < c.siz.Y-1 }

func (c *canvas) canDiagonal(p point) bool {
	return (c.canLeft(p) || c.canRight(p)) && (c.canUp(p) || c.canDown(p))
}

Added 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
712
713
714
715
716
717
718
719
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"testing"
)

func TestNewCanvas(t *testing.T) {
	t.Parallel()
	data := []struct {
		input     []string
		strings   []string
		texts     []string
		points    [][]point
		allPoints bool
	}{
		// 0 Small box
		{
			[]string{
				"+-+",
				"| |",
				"+-+",
			},
			[]string{"Path{[(0,0) (1,0) (2,0) (2,1) (2,2) (1,2) (0,2) (0,1)]}"},
			[]string{""},
			[][]point{{{x: 0, y: 0}, {x: 2, y: 0}, {x: 2, y: 2}, {x: 0, y: 2}}},
			false,
		},

		// 1 Tight box
		{
			[]string{
				"++",
				"++",
			},
			[]string{"Path{[(0,0) (1,0) (1,1) (0,1)]}"},
			[]string{""},
			[][]point{
				{
					{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1},
				},
			},
			false,
		},

		// 2 Indented box
		{
			[]string{
				"",
				" +-+",
				" | |",
				" +-+",
			},
			[]string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"},
			[]string{""},
			[][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}},
			false,
		},

		// 3 Free flow text
		{
			[]string{
				"",
				" foo bar ",
				"b  baz   bee",
			},
			[]string{"Text{(1,1) \"foo bar\"}", "Text{(0,2) \"b  baz\"}", "Text{(9,2) \"bee\"}"},
			[]string{"foo bar", "b  baz", "bee"},
			[][]point{
				{{x: 1, y: 1}, {x: 7, y: 1}},
				{{x: 0, y: 2}, {x: 5, y: 2}},
				{{x: 9, y: 2}, {x: 11, y: 2}},
			},
			false,
		},

		// 4 Text in a box
		{
			[]string{
				"+--+",
				"|Hi|",
				"+--+",
			},
			[]string{"Path{[(0,0) (1,0) (2,0) (3,0) (3,1) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Text{(1,1) \"Hi\"}"},
			[]string{"", "Hi"},
			[][]point{
				{{x: 0, y: 0}, {x: 3, y: 0}, {x: 3, y: 2}, {x: 0, y: 2}},
				{{x: 1, y: 1}, {x: 2, y: 1}},
			},
			false,
		},

		// 5 Concave pieces
		{
			[]string{
				"    +----+",
				"    |    |",
				"+---+    +----+",
				"|             |",
				"+-------------+",
				"", // 5
				"+----+",
				"|    |",
				"|    +---+",
				"|        |",
				"|    +---+", // 10
				"|    |",
				"+----+",
				"",
				"    +----+",
				"    |    |", // 15
				"+---+    |",
				"|        |",
				"+---+    |",
				"    |    |",
				"    +----+",
			},
			[]string{
				"Path{[(4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (9,1) (9,2) (10,2) (11,2) (12,2) (13,2) (14,2) (14,3) (14,4) (13,4) (12,4) (11,4) (10,4) (9,4) (8,4) (7,4) (6,4) (5,4) (4,4) (3,4) (2,4) (1,4) (0,4) (0,3) (0,2) (1,2) (2,2) (3,2) (4,2) (4,1)]}",
				"Path{[(0,6) (1,6) (2,6) (3,6) (4,6) (5,6) (5,7) (5,8) (6,8) (7,8) (8,8) (9,8) (9,9) (9,10) (8,10) (7,10) (6,10) (5,10) (5,11) (5,12) (4,12) (3,12) (2,12) (1,12) (0,12) (0,11) (0,10) (0,9) (0,8) (0,7)]}",
				"Path{[(4,14) (5,14) (6,14) (7,14) (8,14) (9,14) (9,15) (9,16) (9,17) (9,18) (9,19) (9,20) (8,20) (7,20) (6,20) (5,20) (4,20) (4,19) (4,18) (3,18) (2,18) (1,18) (0,18) (0,17) (0,16) (1,16) (2,16) (3,16) (4,16) (4,15)]}",
			},
			[]string{"", "", ""},
			[][]point{
				{
					{x: 4, y: 0}, {x: 9, y: 0}, {x: 9, y: 2}, {x: 14, y: 2},
					{x: 14, y: 4}, {x: 0, y: 4}, {x: 0, y: 2}, {x: 4, y: 2},
				},
				{
					{x: 0, y: 6}, {x: 5, y: 6}, {x: 5, y: 8}, {x: 9, y: 8},
					{x: 9, y: 10}, {x: 5, y: 10}, {x: 5, y: 12}, {x: 0, y: 12},
				},
				{
					{x: 4, y: 14}, {x: 9, y: 14}, {x: 9, y: 20}, {x: 4, y: 20},
					{x: 4, y: 18}, {x: 0, y: 18}, {x: 0, y: 16}, {x: 4, y: 16},
				},
			},
			false,
		},

		// 6 Inner boxes
		{
			[]string{
				"+-----+",
				"|     |",
				"| +-+ |",
				"| | | |",
				"| +-+ |",
				"|     |",
				"+-----+",
			},
			[]string{
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (5,6) (4,6) (3,6) (2,6) (1,6) (0,6) (0,5) (0,4) (0,3) (0,2) (0,1)]}",
				"Path{[(2,2) (3,2) (4,2) (4,3) (4,4) (3,4) (2,4) (2,3)]}",
			},
			[]string{"", ""},
			[][]point{
				{{x: 0, y: 0}, {x: 6, y: 0}, {x: 6, y: 6}, {x: 0, y: 6}},
				{{x: 2, y: 2}, {x: 4, y: 2}, {x: 4, y: 4}, {x: 2, y: 4}},
			},
			false,
		},

		// 7 Real world diagram example
		{
			[]string{
				//         1         2         3
				"      +------+",
				"      |Editor|-------------+--------+",
				"      +------+             |        |",
				"          |                |        v",
				"          v                |   +--------+",
				"      +------+             |   |Document|", // 5
				"      |Window|             |   +--------+",
				"      +------+             |",
				"         |                 |",
				"   +-----+-------+         |",
				"   |             |         |", // 10
				"   v             v         |",
				"+------+     +------+      |",
				"|Window|     |Window|      |",
				"+------+     +------+      |",
				"                |          |", // 15
				"                v          |",
				"              +----+       |",
				"              |View|       |",
				"              +----+       |",
				"                |          |", // 20
				"                v          |",
				"            +--------+     |",
				"            |Document|<----+",
				"            +--------+",
			},
			[]string{
				"Path{[(6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0) (13,1) (13,2) (12,2) (11,2) (10,2) (9,2) (8,2) (7,2) (6,2) (6,1)]}",
				"Path{[(14,1) (15,1) (16,1) (17,1) (18,1) (19,1) (20,1) (21,1) (22,1) (23,1) (24,1) (25,1) (26,1) (27,1) (28,1) (29,1) (30,1) (31,1) (32,1) (33,1) (34,1) (35,1) (36,1) (36,2) (36,3)]}",
				"Path{[(14,1) (15,1) (16,1) (17,1) (18,1) (19,1) (20,1) (21,1) (22,1) (23,1) (24,1) (25,1) (26,1) (27,1) (27,2) (27,3) (27,4) (27,5) (27,6) (27,7) (27,8) (27,9) (27,10) (27,11) (27,12) (27,13) (27,14) (27,15) (27,16) (27,17) (27,18) (27,19) (27,20) (27,21) (27,22) (27,23) (26,23) (25,23) (24,23) (23,23) (22,23)]}",
				"Path{[(10,3) (10,4)]}",
				"Path{[(31,4) (32,4) (33,4) (34,4) (35,4) (36,4) (37,4) (38,4) (39,4) (40,4) (40,5) (40,6) (39,6) (38,6) (37,6) (36,6) (35,6) (34,6) (33,6) (32,6) (31,6) (31,5)]}",
				"Path{[(6,5) (7,5) (8,5) (9,5) (10,5) (11,5) (12,5) (13,5) (13,6) (13,7) (12,7) (11,7) (10,7) (9,7) (8,7) (7,7) (6,7) (6,6)]}",
				"Path{[(9,8) (9,9)]}",
				"Path{[(9,9) (8,9) (7,9) (6,9) (5,9) (4,9) (3,9) (3,10) (3,11)]}",
				"Path{[(9,9) (10,9) (11,9) (12,9) (13,9) (14,9) (15,9) (16,9) (17,9) (17,10) (17,11)]}",
				"Path{[(0,12) (1,12) (2,12) (3,12) (4,12) (5,12) (6,12) (7,12) (7,13) (7,14) (6,14) (5,14) (4,14) (3,14) (2,14) (1,14) (0,14) (0,13)]}",
				"Path{[(13,12) (14,12) (15,12) (16,12) (17,12) (18,12) (19,12) (20,12) (20,13) (20,14) (19,14) (18,14) (17,14) (16,14) (15,14) (14,14) (13,14) (13,13)]}",
				"Path{[(16,15) (16,16)]}",
				"Path{[(14,17) (15,17) (16,17) (17,17) (18,17) (19,17) (19,18) (19,19) (18,19) (17,19) (16,19) (15,19) (14,19) (14,18)]}",
				"Path{[(16,20) (16,21)]}",
				"Path{[(12,22) (13,22) (14,22) (15,22) (16,22) (17,22) (18,22) (19,22) (20,22) (21,22) (21,23) (21,24) (20,24) (19,24) (18,24) (17,24) (16,24) (15,24) (14,24) (13,24) (12,24) (12,23)]}",
				"Text{(7,1) \"Editor\"}",
				"Text{(32,5) \"Document\"}",
				"Text{(7,6) \"Window\"}",
				"Text{(1,13) \"Window\"}",
				"Text{(14,13) \"Window\"}",
				"Text{(15,18) \"View\"}",
				"Text{(13,23) \"Document\"}",
			},
			[]string{
				"", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
				"Editor", "Document", "Window", "Window", "Window", "View", "Document",
			},
			[][]point{
				{{x: 6, y: 0}, {x: 13, y: 0}, {x: 13, y: 2}, {x: 6, y: 2}},
				{{x: 14, y: 1}, {x: 36, y: 1}, {x: 36, y: 3, hint: 3}},
				{{x: 14, y: 1}, {x: 27, y: 1}, {x: 27, y: 23}, {x: 22, y: 23, hint: 3}},
				{{x: 10, y: 3}, {x: 10, y: 4, hint: 3}},
				{{x: 31, y: 4}, {x: 40, y: 4}, {x: 40, y: 6}, {x: 31, y: 6}},
				{{x: 6, y: 5}, {x: 13, y: 5}, {x: 13, y: 7}, {x: 6, y: 7}},
				{{x: 9, y: 8}, {x: 9, y: 9}},
				{{x: 9, y: 9}, {x: 3, y: 9}, {x: 3, y: 11, hint: 3}},
				{{x: 9, y: 9}, {x: 17, y: 9}, {x: 17, y: 11, hint: 3}},
				{{x: 0, y: 12}, {x: 7, y: 12}, {x: 7, y: 14}, {x: 0, y: 14}},
				{{x: 13, y: 12}, {x: 20, y: 12}, {x: 20, y: 14}, {x: 13, y: 14}},
				{{x: 16, y: 15}, {x: 16, y: 16, hint: 3}},
				{{x: 14, y: 17}, {x: 19, y: 17}, {x: 19, y: 19}, {x: 14, y: 19}},
				{{x: 16, y: 20}, {x: 16, y: 21, hint: 3}},
				{{x: 12, y: 22}, {x: 21, y: 22}, {x: 21, y: 24}, {x: 12, y: 24}},
				{{x: 7, y: 1}, {x: 12, y: 1}},
				{{x: 32, y: 5}, {x: 39, y: 5}},
				{{x: 7, y: 6}, {x: 12, y: 6}},
				{{x: 1, y: 13}, {x: 6, y: 13}},
				{{x: 14, y: 13}, {x: 19, y: 13}},
				{{x: 15, y: 18}, {x: 18, y: 18}},
				{{x: 13, y: 23}, {x: 20, y: 23}},
			},
			false,
		},

		// 8 Interwined lines.
		{
			[]string{
				"             +-----+-------+",
				"             |     |       |",
				"             |     |       |",
				"        +----+-----+----   |",
				"--------+----+-----+-------+---+",
				"        |    |     |       |   |",
				"        |    |     |       |   |     |   |",
				"        |    |     |       |   |     |   |",
				"        |    |     |       |   |     |   |",
				"--------+----+-----+-------+---+-----+---+--+",
				"        |    |     |       |   |     |   |  |",
				"        |    |     |       |   |     |   |  |",
				"        |   -+-----+-------+---+-----+   |  |",
				"        |    |     |       |   |     |   |  |",
				"        |    |     |       |   +-----+---+--+",
				"             |     |       |         |   |",
				"             |     |       |         |   |",
				"     --------+-----+-------+---------+---+-----",
				"             |     |       |         |   |",
				"             +-----+-------+---------+---+",
			},
			// TODO(dhobsd): it's a tad overwhelming.
			nil,
			nil,
			nil,
			false,
		},

		// 9 Indented box
		{
			[]string{
				"",
				" +-+",
				" | |",
				" +-+",
			},
			[]string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"},
			[]string{""},
			[][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}},
			false,
		},

		// 10 Diagonal lines with arrows
		{
			[]string{
				"^          ^",
				" \\        /",
				"  \\      /",
				"   \\    /",
				"    v  v",
			},
			[]string{"Path{[(0,0) (1,1) (2,2) (3,3) (4,4)]}", "Path{[(11,0) (10,1) (9,2) (8,3) (7,4)]}"},
			[]string{"", ""},
			[][]point{
				{{x: 0, y: 0, hint: 2}, {x: 4, y: 4, hint: 3}},
				{{x: 11, y: 0, hint: 2}, {x: 7, y: 4, hint: 3}},
			},
			false,
		},

		// 11 Diagonal lines forming an object
		{
			[]string{
				"   .-----.",
				"  /       \\",
				" /         \\",
				"+           +",
				"|           |",
				"|           |",
				"+           +",
				" \\         /",
				"  \\       /",
				"   '-----'",
			},
			[]string{"Path{[(3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,1) (11,2) (12,3) (12,4) (12,5) (12,6) (11,7) (10,8) (9,9) (8,9) (7,9) (6,9) (5,9) (4,9) (3,9) (2,8) (1,7) (0,6) (0,5) (0,4) (0,3) (1,2) (2,1)]}"},
			[]string{""},
			[][]point{{
				{x: 3, y: 0},
				{x: 9, y: 0},
				{x: 12, y: 3},
				{x: 12, y: 6},
				{x: 9, y: 9},
				{x: 3, y: 9},
				{x: 0, y: 6},
				{x: 0, y: 3},
			}},
			false,
		},

		// 12 A2S logo
		{
			[]string{
				".-------------------------.",
				"|                         |",
				"| .---.-. .-----. .-----. |",
				"| | .-. | +-->  | |  <--+ |",
				"| | '-' | |  <--+ +-->  | |",
				"| '---'-' '-----' '-----' |",
				"|  ascii     2      svg   |",
				"|                         |",
				"'-------------------------'",
			},
			[]string{
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0) (14,0) (15,0) (16,0) (17,0) (18,0) (19,0) (20,0) (21,0) (22,0) (23,0) (24,0) (25,0) (26,0) (26,1) (26,2) (26,3) (26,4) (26,5) (26,6) (26,7) (26,8) (25,8) (24,8) (23,8) (22,8) (21,8) (20,8) (19,8) (18,8) (17,8) (16,8) (15,8) (14,8) (13,8) (12,8) (11,8) (10,8) (9,8) (8,8) (7,8) (6,8) (5,8) (4,8) (3,8) (2,8) (1,8) (0,8) (0,7) (0,6) (0,5) (0,4) (0,3) (0,2) (0,1)]}",
				"Path{[(2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (8,3) (8,4) (8,5) (7,5) (6,5) (5,5) (4,5) (3,5) (2,5) (2,4) (2,3)]}",
				"Path{[(2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (8,3) (8,4) (8,5) (7,5) (6,5) (6,4) (5,4) (4,4) (4,3) (5,3) (6,3)]}",
				"Path{[(10,2) (11,2) (12,2) (13,2) (14,2) (15,2) (16,2) (16,3) (16,4) (15,4) (14,4) (13,4)]}",
				"Path{[(10,2) (11,2) (12,2) (13,2) (14,2) (15,2) (16,2) (16,3) (16,4) (16,5) (15,5) (14,5) (13,5) (12,5) (11,5) (10,5) (10,4) (10,3)]}",
				"Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (23,3) (22,3) (21,3)]}",
				"Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (24,4) (24,5) (23,5) (22,5) (21,5) (20,5) (19,5) (18,5) (18,4) (19,4) (20,4) (21,4)]}",
				"Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (24,4) (24,5) (23,5) (22,5) (21,5) (20,5) (19,5) (18,5) (18,4) (18,3)]}",
				"Path{[(10,3) (11,3) (12,3) (13,3)]}",
				"Text{(3,6) \"ascii\"}",
				"Text{(13,6) \"2\"}",
				"Text{(20,6) \"svg\"}",
			},
			[]string{"", "", "", "", "", "", "", "", "", "ascii", "2", "svg"},
			[][]point{
				{{x: 0, y: 0}, {x: 26, y: 0}, {x: 26, y: 8}, {x: 0, y: 8}},
				{{x: 2, y: 2}, {x: 8, y: 2}, {x: 8, y: 5}, {x: 2, y: 5}},
				{{x: 2, y: 2}, {x: 8, y: 2}, {x: 8, y: 5}, {x: 6, y: 5}, {x: 6, y: 4}, {x: 4, y: 4}, {x: 4, y: 3}, {x: 6, y: 3}},
				{{x: 10, y: 2}, {x: 16, y: 2}, {x: 16, y: 4}, {x: 13, y: 4, hint: 3}},
				{{x: 10, y: 2}, {x: 16, y: 2}, {x: 16, y: 5}, {x: 10, y: 5}},
				{{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 3}, {x: 21, y: 3, hint: 3}},
				{{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 5}, {x: 18, y: 5}, {x: 18, y: 4}, {x: 21, y: 4, hint: 3}},
				{{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 5}, {x: 18, y: 5}},
				{{x: 10, y: 3}, {x: 13, y: 3, hint: 3}},
				{{x: 3, y: 6}, {x: 7, y: 6}},
				{{x: 13, y: 6}},
				{{x: 20, y: 6}, {x: 22, y: 6}},
			},
			false,
		},

		// 13 Ticks and dots in lines.
		{
			[]string{
				" ------x----->",
				"",
				" <-----*------",
			},
			[]string{"Path{[(1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0)]}", "Path{[(1,2) (2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (9,2) (10,2) (11,2) (12,2) (13,2)]}"},
			[]string{"", ""},
			[][]point{
				{
					{x: 1, y: 0, hint: 0},
					{x: 2, y: 0, hint: 0},
					{x: 3, y: 0, hint: 0},
					{x: 4, y: 0, hint: 0},
					{x: 5, y: 0, hint: 0},
					{x: 6, y: 0, hint: 0},
					{x: 7, y: 0, hint: 4},
					{x: 8, y: 0, hint: 0},
					{x: 9, y: 0, hint: 0},
					{x: 10, y: 0, hint: 0},
					{x: 11, y: 0, hint: 0},
					{x: 12, y: 0, hint: 0},
					{x: 13, y: 0, hint: 3},
				},
				{
					{x: 1, y: 2, hint: 2},
					{x: 2, y: 2, hint: 0},
					{x: 3, y: 2, hint: 0},
					{x: 4, y: 2, hint: 0},
					{x: 5, y: 2, hint: 0},
					{x: 6, y: 2, hint: 0},
					{x: 7, y: 2, hint: 5},
					{x: 8, y: 2, hint: 0},
					{x: 9, y: 2, hint: 0},
					{x: 10, y: 2, hint: 0},
					{x: 11, y: 2, hint: 0},
					{x: 12, y: 2, hint: 0},
					{x: 13, y: 2, hint: 0},
				},
			},
			true,
		},

		// 14 Multiple closed path on one object
		{
			[]string{
				"+-+-+",
				"| | |",
				"+-+-+",
			},
			[]string{
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}",
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}", // TODO (2,0)
			},
			[]string{"", ""},
			[][]point{
				{
					{0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0},
					{3, 2, 0}, {2, 2, 0}, {1, 2, 0}, {0, 2, 0}, {0, 1, 0},
				},
				{
					{0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0},
					{3, 2, 0}, {2, 2, 0}, {2, 1, 0}, // TODO: {2, 0, 0}
				},
			},
			true,
		},
	}
	for i, line := range data {
		c, err := newCanvas([]byte(strings.Join(line.input, "\n")))
		if err != nil {
			t.Fatalf("Test %d: error creating canvas: %s", i, err)
		}
		objs := c.objects()
		if line.strings != nil {
			if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) {
				t.Errorf("%d: expected %q, but got %q", i, line.strings, got)
			}
		}
		if line.texts != nil {
			if got := getTexts(objs); !reflect.DeepEqual(line.texts, got) {
				t.Errorf("%d: expected %q, but got %q", i, line.texts, got)
			}
		}
		if line.points != nil {
			if line.allPoints == false {
				if got := getCorners(objs); !reflect.DeepEqual(line.points, got) {
					t.Errorf("%d: expected %q, but got %q", i, line.points, got)
				}
			} else {
				if got := getPoints(objs); !reflect.DeepEqual(line.points, got) {
					t.Errorf("%d: expected %q, but got %q", i, line.points, got)
				}
			}
		}
	}
}

func TestNewCanvasBroken(t *testing.T) {
	// These are the ones that do not give the desired result.
	t.Parallel()
	data := []struct {
		input   []string
		strings []string
		texts   []string
		corners [][]point
	}{
		// 0 URL
		{
			[]string{
				"github.com/foo/bar",
			},
			[]string{"Text{(0,0) \"github.com/foo/bar\"}"},
			[]string{"github.com/foo/bar"},
			[][]point{{{x: 0, y: 0}, {x: 17, y: 0}}},
		},

		// 1 Merged boxes
		{
			[]string{
				"+-+-+",
				"| | |",
				"+-+-+",
			},
			[]string{"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}"},
			[]string{"", ""},
			// TODO(dhobsd): BROKEN.
			[][]point{
				{{x: 0, y: 0}, {x: 4, y: 0}, {x: 4, y: 2}, {x: 0, y: 2}},
				{{x: 0, y: 0}, {x: 4, y: 0}, {x: 4, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}},
			},
		},

		// 2 Adjacent boxes
		{
			// TODO(dhobsd): BROKEN. This one is hard, as it can be seen as 3 boxes
			// but that is not what is desired.
			[]string{
				"+-++-+",
				"| || |",
				"+-++-+",
			},
			[]string{
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}",
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (2,2) (2,1)]}",
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (3,1)]}",
			},
			[]string{"", "", ""},
			[][]point{
				{{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 0, y: 2}},
				{{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}},
				{{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 3, y: 2}, {x: 3, y: 1}},
			},
		},
	}
	for i, line := range data {
		c, err := newCanvas([]byte(strings.Join(line.input, "\n")))
		if err != nil {
			t.Fatalf("Test %d: error creating canvas: %s", i, err)
		}
		objs := c.objects()
		if line.strings != nil {
			if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) {
				t.Errorf("%d: expected %q, but got %q", i, line.strings, got)
			}
		}
		if line.texts != nil {
			if got := getTexts(objs); !reflect.DeepEqual(line.texts, got) {
				t.Errorf("%d: expected %q, but got %q", i, line.texts, got)
			}
		}
		if line.corners != nil {
			if got := getCorners(objs); !reflect.DeepEqual(line.corners, got) {
				t.Errorf("%d: expected %q, but got %q", i, line.corners, got)
			}
		}
	}
}

func TestPointsToCorners(t *testing.T) {
	t.Parallel()
	data := []struct {
		in       []point
		expected []point
		closed   bool
	}{
		{
			[]point{{x: 0, y: 0}, {x: 1, y: 0}},
			[]point{{x: 0, y: 0}, {x: 1, y: 0}},
			false,
		},
		{
			[]point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}},
			[]point{{x: 0, y: 0}, {x: 2, y: 0}},
			false,
		},
		{
			[]point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}},
			[]point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}},
			false,
		},
		{
			[]point{
				{x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}, {x: 2, y: 1}, {x: 2, y: 2},
				{x: 1, y: 2}, {x: 0, y: 2}, {x: 0, y: 1},
			},
			[]point{{x: 0, y: 0}, {x: 2, y: 0}, {x: 2, y: 2}, {x: 0, y: 2}},
			true,
		},
		{
			[]point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}},
			[]point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}},
			// TODO(dhobsd): Unexpected; broken.
			false,
		},
	}
	for i, line := range data {
		p, c := pointsToCorners(line.in)
		if !reflect.DeepEqual(line.expected, p) {
			t.Errorf("%d: expected %v, but got %v", i, line.expected, p)
		}
		if line.closed != c {
			t.Errorf("%d: expected close == %v, but got %v", i, line.closed, c)
		}
	}
}

func BenchmarkT(b *testing.B) {
	data := []string{
		"             +-----+-------+",
		"             |     |       |",
		"             |     |       |",
		"        +----+-----+----   |",
		"--------+----+-----+-------+---+",
		"        |    |     |       |   |",
		"        |    |     |       |   |     |   |",
		"        |    |     |       |   |     |   |",
		"        |    |     |       |   |     |   |",
		"--------+----+-----+-------+---+-----+---+--+",
		"        |    |     |       |   |     |   |  |",
		"        |    |     |       |   |     |   |  |",
		"        |   -+-----+-------+---+-----+   |  |",
		"        |    |     |       |   |     |   |  |",
		"        |    |     |       |   +-----+---+--+",
		"             |     |       |         |   |",
		"             |     |       |         |   |",
		"     --------+-----+-------+---------+---+-----",
		"             |     |       |         |   |",
		"             +-----+-------+---------+---+",
		"",
		"",
	}
	chunk := []byte(strings.Join(data, "\n"))
	input := make([]byte, 0, len(chunk)*b.N)
	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)
	}

	objs := c.objects()
	if len(objs) != expected {
		b.Fatalf("%d != %d", len(objs), expected)
	}
}

// Private details.

func getPoints(objs []*object) [][]point {
	out := [][]point{}
	for _, obj := range objs {
		out = append(out, obj.Points())
	}
	return out
}

func getTexts(objs []*object) []string {
	out := []string{}
	for _, obj := range objs {
		t := obj.Text()
		if !obj.isJustText() {
			out = append(out, "")
		} else if len(t) > 0 {
			out = append(out, string(t))
		} else {
			panic("failed")
		}
	}
	return out
}

func getStrings(objs []*object) []string {
	out := []string{}
	for _, obj := range objs {
		out = append(out, obj.String())
	}
	return out
}

func getCorners(objs []*object) [][]point {
	out := make([][]point, len(objs))
	for i, obj := range objs {
		out[i] = obj.Corners()
	}
	return out
}

Added 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 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

func (c char) isTextStart() bool {
	r := rune(c)
	return unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSymbol(r)
}

func (c char) isTextCont() bool {
	return unicode.IsPrint(rune(c))
}

func (c char) isSpace() bool {
	return unicode.IsSpace(rune(c))
}

// isPathStart returns true on any form of ascii art that can start a graph.
func (c char) isPathStart() bool {
	return (c.isCorner() || c.isHorizontal() || c.isVertical() || c.isArrowHorizontalLeft() || c.isArrowVerticalUp() || c.isDiagonal()) &&
		!c.isTick() && !c.isDot()
}

func (c char) isCorner() bool {
	return c == '.' || c == '\'' || c == '+'
}

func (c char) isRoundedCorner() bool {
	return c == '.' || c == '\''
}

func (c char) isDashedHorizontal() bool {
	return c == '='
}

func (c char) isHorizontal() bool {
	return c.isDashedHorizontal() || c.isTick() || c.isDot() || c == '-'
}

func (c char) isDashedVertical() bool {
	return c == ':'
}

func (c char) isVertical() bool {
	return c.isDashedVertical() || c.isTick() || c.isDot() || c == '|'
}

func (c char) isDashed() bool {
	return c.isDashedHorizontal() || c.isDashedVertical()
}

func (c char) isArrowHorizontalLeft() bool {
	return c == '<'
}

func (c char) isArrowHorizontal() bool {
	return c.isArrowHorizontalLeft() || c == '>'
}

func (c char) isArrowVerticalUp() bool {
	return c == '^'
}

func (c char) isArrowVertical() bool {
	return c.isArrowVerticalUp() || c == 'v'
}

func (c char) isArrow() bool {
	return c.isArrowHorizontal() || c.isArrowVertical()
}

func (c char) isDiagonalNorthEast() bool {
	return c == '/'
}

func (c char) isDiagonalSouthEast() bool {
	return c == '\\'
}

func (c char) isDiagonal() bool {
	return c.isDiagonalNorthEast() || c.isDiagonalSouthEast()
}

func (c char) isTick() bool {
	return c == 'x'
}

func (c char) isDot() bool {
	return c == '*'
}

// Diagonal transitions are special: you can move lines diagonally, you can move diagonally from
// corners to edges or lines, but you cannot move diagonally between corners.
func (c char) canDiagonalFrom(from char) bool {
	if from.isArrowVertical() || from.isCorner() {
		return c.isDiagonal()
	}
	if from.isDiagonal() {
		return c.isDiagonal() || c.isCorner() || c.isArrowVertical() || c.isHorizontal() || c.isVertical()
	}
	if from.isHorizontal() || from.isVertical() {
		return c.isDiagonal()
	}
	return false
}

func (c char) canHorizontal() bool {
	return c.isHorizontal() || c.isCorner() || c.isArrowHorizontal()
}

func (c char) canVertical() bool {
	return c.isVertical() || c.isCorner() || c.isArrowVertical()
}

Added 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/attrs"
	"zettelstore.de/client.fossil/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,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

const (
	defaultFont   = ""
	defaultScaleX = 10
	defaultScaleY = 20
)

func parseBlocks(inp *input.Input, m *meta.Meta, _ string) ast.BlockSlice {
	font := m.GetDefault("font", defaultFont)
	scaleX := m.GetNumber("x-scale", defaultScaleX)
	scaleY := m.GetNumber("y-scale", defaultScaleY)
	if scaleX < 1 || 1000000 < scaleX {
		scaleX = defaultScaleX
	}
	if scaleY < 1 || 1000000 < scaleY {
		scaleY = defaultScaleY
	}

	canvas, err := newCanvas(inp.Src[inp.Pos:])
	if err != nil {
		return ast.BlockSlice{ast.CreateParaNode(canvasErrMsg(err)...)}
	}
	svg := canvasToSVG(canvas, font, int(scaleX), int(scaleY))
	if len(svg) == 0 {
		return ast.BlockSlice{ast.CreateParaNode(noSVGErrMsg()...)}
	}
	return ast.BlockSlice{&ast.BLOBNode{
		Description: parser.ParseDescription(m),
		Syntax:      meta.SyntaxSVG,
		Blob:        svg,
	}}
}

func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
	canvas, err := newCanvas(inp.Src[inp.Pos:])
	if err != nil {
		return canvasErrMsg(err)
	}
	svg := canvasToSVG(canvas, defaultFont, defaultScaleX, defaultScaleY)
	if len(svg) == 0 {
		return noSVGErrMsg()
	}
	return ast.InlineSlice{&ast.EmbedBLOBNode{
		Attrs:   nil,
		Syntax:  meta.SyntaxSVG,
		Blob:    svg,
		Inlines: nil,
	}}
}

// ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB.
func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode {
	font := defaultFont
	if val, found := vn.Attrs.Get("font"); found {
		font = val
	}
	scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX)
	scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY)

	canvas, err := newCanvas(vn.Content)
	if err != nil {
		return ast.CreateParaNode(canvasErrMsg(err)...)
	}
	if scaleX < 1 || 1000000 < scaleX {
		scaleX = defaultScaleX
	}
	if scaleY < 1 || 1000000 < scaleY {
		scaleY = defaultScaleY
	}
	svg := canvasToSVG(canvas, font, scaleX, scaleY)
	if len(svg) == 0 {
		return ast.CreateParaNode(noSVGErrMsg()...)
	}
	return &ast.BLOBNode{
		Description: nil, // TODO: look for attribute "summary" / "title"
		Syntax:      meta.SyntaxSVG,
		Blob:        svg,
	}
}

func getScale(a attrs.Attributes, key string, defVal int) int {
	if val, found := a.Get(key); found {
		if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 {
			return n
		}
	}
	return defVal
}

func canvasErrMsg(err error) ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("Error:", err.Error())
}

func noSVGErrMsg() ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("NO", "IMAGE")
}

Added 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
27
28
29
30
31
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/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)
	})
}

Added 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

// 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
	isText   bool
	isClosed bool
	isDashed bool
}

// Points returns all the points occupied by this Object. Every object has at least one point,
// and all points are both in-order and contiguous.
func (o *object) Points() []point { return o.points }

// Corners returns all the corners (change of direction) along the path.
func (o *object) Corners() []point { return o.corners }

// IsClosed is true if the object is composed of a closed path.
func (o *object) IsClosed() bool { return o.isClosed }

// IsDashed is true if this object is a path object, and lines should be drawn dashed.
func (o *object) IsDashed() bool { return o.isDashed }

// Text returns the text associated with this object if textual, and nil otherwise.
func (o *object) Text() []rune {
	return o.text
}

func (o *object) isOpenPath() bool   { return !o.isClosed && !o.isText }
func (o *object) isClosedPath() bool { return o.isClosed && !o.isText }
func (o *object) isJustText() bool   { return o.isText }

func (o *object) String() string {
	if o.isJustText() {
		return fmt.Sprintf("Text{%s %q}", o.points[0], string(o.text))
	}
	return fmt.Sprintf("Path{%v}", o.points)
}

// seal finalizes the object, setting its text, its corners, and its various rendering hints.
func (o *object) seal(c *canvas) {
	if c.at(o.points[0]).isArrow() {
		o.points[0].hint = startMarker
		c.hasStartMarker = true
	}

	if c.at(o.points[len(o.points)-1]).isArrow() {
		o.points[len(o.points)-1].hint = endMarker
		c.hasEndMarker = true
	}

	o.corners, o.isClosed = pointsToCorners(o.points)
	o.text = make([]rune, len(o.points))

	for i, p := range o.points {
		ch := c.at(p)
		if !o.isJustText() {
			if ch.isTick() {
				o.points[i].hint = tick
			} else if ch.isDot() {
				o.points[i].hint = dot
			}
			if ch.isDashed() {
				o.isDashed = true
			}

			for _, corner := range o.corners {
				if corner.x == p.x && corner.y == p.y && c.at(p).isRoundedCorner() {
					o.points[i].hint = roundedCorner
				}
			}
		}
		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] }

// 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
	}
	lp := l.Points()[0]
	rp := r.Points()[0]
	if lp.y != rp.y {
		return lp.y < rp.y
	}
	return lp.x < rp.x
}

const (
	dirNone = iota // No directionality
	dirH           // Horizontal
	dirV           // Vertical
	dirSE          // South-East
	dirSW          // South-West
	dirNW          // North-West
	dirNE          // North-East
)

// pointsToCorners returns all the corners (points at which there is a change of directionality) for
// a path. It additionally returns a truth value indicating whether the points supplied indicate a
// closed path.
func pointsToCorners(points []point) ([]point, bool) {
	l := len(points)
	// A path containing fewer than 3 points can neither be closed, nor change direction.
	if l < 3 {
		return points, false
	}
	out := []point{points[0]}

	dir := dirNone
	if isHorizontal(points[0], points[1]) {
		dir = dirH
	} else if isVertical(points[0], points[1]) {
		dir = dirV
	} else if isDiagonalSE(points[0], points[1]) {
		dir = dirSE
	} else if isDiagonalSW(points[0], points[1]) {
		dir = dirSW
	} else if isDiagonalNW(points[0], points[1]) {
		dir = dirNW
	} else if isDiagonalNE(points[0], points[1]) {
		dir = dirNE
	} else {
		panic(fmt.Errorf("discontiguous points: %+v", points))
	}

	cornerFunc := func(idx, newDir int) {
		if dir != newDir {
			out = append(out, points[idx-1])
			dir = newDir
		}
	}

	// Starting from the third point, check to see if the directionality between points P and
	// P-1 has changed.
	for i := 2; i < l; i++ {
		if isHorizontal(points[i-1], points[i]) {
			cornerFunc(i, dirH)
		} else if isVertical(points[i-1], points[i]) {
			cornerFunc(i, dirV)
		} else if isDiagonalSE(points[i-1], points[i]) {
			cornerFunc(i, dirSE)
		} else if isDiagonalSW(points[i-1], points[i]) {
			cornerFunc(i, dirSW)
		} else if isDiagonalNW(points[i-1], points[i]) {
			cornerFunc(i, dirNW)
		} else if isDiagonalNE(points[i-1], points[i]) {
			cornerFunc(i, dirNE)
		} else {
			panic(fmt.Errorf("discontiguous points: %+v", points))
		}
	}

	// Check if the points indicate a closed path. If not, append the last point.
	last := points[l-1]
	closed := true
	closedFunc := func(newDir int) {
		if dir != newDir {
			closed = false
			out = append(out, last)
		}
	}
	if isHorizontal(points[0], last) {
		closedFunc(dirH)
	} else if isVertical(points[0], last) {
		closedFunc(dirV)
	} else if isDiagonalNE(last, points[0]) {
		closedFunc(dirNE)
	} else {
		// Note: we'll always find any closed polygon from its top-left-most point. If it
		// is closed, it must be closed in the north-easterly direction, thus we don't test
		// for any other types of polygone closure.
		closed = false
		out = append(out, last)
	}

	return out, closed
}

Added 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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) 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.
type renderHint uint8

const (
	_             renderHint = iota
	roundedCorner            // the renderer should smooth corners on this path.
	startMarker              // this point should have an SVG marker-start attribute.
	endMarker                // this point should have an SVG marker-end attribute.
	tick                     // the renderer should mark a tick in the path at this point.
	dot                      // the renderer should insert a filled dot in the path at this point.
)

// A point is an X,Y coordinate in the diagram's grid. The grid represents (0, 0) as the top-left
// of the diagram. The point also provides hints to the renderer as to how it should be interpreted.
type point struct {
	x, y int
	hint renderHint
}

// String implements fmt.Stringer on Point.
func (p point) String() string { return fmt.Sprintf("(%d,%d)", p.x, p.y) }

// isHorizontal returns true if p1 and p2 are horizontally aligned.
func isHorizontal(p1, p2 point) bool {
	d := p1.x - p2.x
	return d <= 1 && d >= -1 && p1.y == p2.y
}

// isVertical returns true if p1 and p2 are vertically aligned.
func isVertical(p1, p2 point) bool {
	d := p1.y - p2.y
	return d <= 1 && d >= -1 && p1.x == p2.x
}

// The following functions return true when the diagonals are connected in various compass directions.
func isDiagonalSE(p1, p2 point) bool { return p1.x-p2.x == -1 && p1.y-p2.y == -1 }
func isDiagonalSW(p1, p2 point) bool { return p1.x-p2.x == 1 && p1.y-p2.y == -1 }
func isDiagonalNW(p1, p2 point) bool { return p1.x-p2.x == 1 && p1.y-p2.y == 1 }
func isDiagonalNE(p1, p2 point) bool { return p1.x-p2.x == -1 && p1.y-p2.y == 1 }

Added 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"io"
	"strings"

	"zettelstore.de/z/strfun"
)

// canvasToSVG renders the supplied asciitosvg.Canvas to SVG, based on the supplied options.
func canvasToSVG(c *canvas, font string, scaleX, scaleY int) []byte {
	if len(c.objects()) == 0 {
		return nil
	}
	if font == "" {
		font = "monospace"
	}

	var b bytes.Buffer
	fmt.Fprintf(&b,
		`<svg class="zs-draw" width="%d" height="%d" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`,
		(c.size().X+1)*scaleX, (c.size().Y+1)*scaleY)
	writeMarkerDefs(&b, c, scaleX, scaleY)

	// 3 passes, first closed paths, then open paths, then text.
	writeClosedPaths(&b, c, scaleX, scaleY)
	writeOpenPaths(&b, c, scaleX, scaleY)
	writeTexts(&b, c, escape(font), scaleX, scaleY)

	io.WriteString(&b, "</svg>")
	return b.Bytes()
}

const (
	nameStartMarker = "iPointer"
	nameEndMarker   = "Pointer"
)

func writeMarkerDefs(w io.Writer, c *canvas, scaleX, scaleY int) {
	const markerTag = `<marker id="%s" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="%g" markerHeight="%g" orient="auto"><path d="%s" /></marker>`
	x := float64(scaleX) / 2
	y := float64(scaleY) / 2
	if c.hasStartMarker {
		fmt.Fprintf(w, markerTag, nameStartMarker, x, y, "M 10 0 L 10 10 L 0 5 z")
	}
	if c.hasEndMarker {
		fmt.Fprintf(w, markerTag, nameEndMarker, x, y, "M 0 0 L 10 5 L 0 10 z")
	}
}

const pathTag = `<path id="%s%d" %sd="%s" />`

func writeClosedPaths(w io.Writer, c *canvas, scaleX, scaleY int) {
	first := true
	for i, obj := range c.objects() {
		if !obj.isClosedPath() {
			continue
		}
		if first {
			io.WriteString(w, `<g id="closed" stroke="#000" stroke-width="2" fill="none">`)
			first = false
		}
		opts := ""
		if obj.IsDashed() {
			opts = `stroke-dasharray="5 5" `
		}

		fmt.Fprintf(w, pathTag, "closed", i, opts, flatten(obj.Points(), scaleX, scaleY)+"Z")
	}
	if !first {
		io.WriteString(w, "</g>")
	}
}

func writeOpenPaths(w io.Writer, c *canvas, scaleX, scaleY int) {
	const optStartMarker = `marker-start="url(#` + nameStartMarker + `)" `
	const optEndMarker = `marker-end="url(#` + nameEndMarker + `)" `

	first := true
	for i, obj := range c.objects() {
		if !obj.isOpenPath() {
			continue
		}
		if first {
			io.WriteString(w, `<g id="lines" stroke="#000" stroke-width="2" fill="none">`)
			first = false
		}
		points := obj.Points()
		for _, p := range points {
			switch p.hint {
			case dot:
				sp := scale(p, scaleX, scaleY)
				fmt.Fprintf(w, `<circle cx="%g" cy="%g" r="3" fill="#000" />`, sp.X, sp.Y)
			case tick:
				sp := scale(p, scaleX, scaleY)
				const tickLine = `<line x1="%g" y1="%g" x2="%g" y2="%g" stroke-width="2" />`
				fmt.Fprintf(w, tickLine, sp.X-4, sp.Y-4, sp.X+4, sp.Y+4)
				fmt.Fprintf(w, tickLine, sp.X+4, sp.Y-4, sp.X-4, sp.Y+4)
			}
		}

		opts := ""
		if obj.IsDashed() {
			opts += `stroke-dasharray="5 5" `
		}
		if points[0].hint == startMarker {
			opts += optStartMarker
		}
		if points[len(points)-1].hint == endMarker {
			opts += optEndMarker
		}
		fmt.Fprintf(w, pathTag, "open", i, opts, flatten(points, scaleX, scaleY))
	}
	if !first {
		io.WriteString(w, "</g>")
	}
}

func writeTexts(w io.Writer, c *canvas, font string, scaleX, scaleY int) {
	fontSize := float64(scaleY) * 0.75
	deltaX := float64(scaleX) / 4
	deltaY := float64(scaleY) / 4
	first := true
	for i, obj := range c.objects() {
		if !obj.isJustText() {
			continue
		}
		if first {
			fmt.Fprintf(w, `<g id="text" stroke="none" style="font-family:%s;font-size:%gpx">`, font, fontSize)
			first = false
		}

		text := string(obj.Text())
		sp := scale(obj.Points()[0], scaleX, scaleY)
		fmt.Fprintf(w,
			`<text id="obj%d" x="%g" y="%g">%s</text>`,
			i, sp.X-deltaX, sp.Y+deltaY, escape(text))
	}
	if !first {
		io.WriteString(w, "</g>")
	}
}

func escape(s string) string {
	var sb strings.Builder
	strfun.XMLEscape(&sb, s)
	return sb.String()
}

type scaledPoint struct {
	X    float64
	Y    float64
	Hint renderHint
}

func scale(p point, scaleX, scaleY int) scaledPoint {
	return scaledPoint{
		X:    (float64(p.x) + .5) * float64(scaleX),
		Y:    (float64(p.y) + .5) * float64(scaleY),
		Hint: p.hint,
	}
}

func flatten(points []point, scaleX, scaleY int) string {
	var result strings.Builder

	// Scaled start point, and previous point (which is always initially the start point).
	sp := scale(points[0], scaleX, scaleY)
	pp := sp

	for i, cp := range points {
		p := scale(cp, scaleX, scaleY)

		// Our start point is represented by a single moveto command (unless the start point
		// is a rounded corner) as the shape will be closed with the Z command automatically
		// if we have a closed polygon. If our start point is a rounded corner, we have to go
		// ahead and draw that curve.
		if i == 0 {
			if cp.hint == roundedCorner {
				fmt.Fprintf(&result, "M %g %g Q %g %g %g %g ", p.X, p.Y+10, p.X, p.Y, p.X+10, p.Y)
				continue
			}

			fmt.Fprintf(&result, "M %g %g ", p.X, p.Y)
			continue
		}

		// If this point has a rounded corner, we need to calculate the curve. This algorithm
		// only works when the shapes are drawn in a clockwise manner.
		if cp.hint == roundedCorner {
			// The control point is always the original corner.
			cx := p.X
			cy := p.Y

			sx, sy, ex, ey := 0., 0., 0., 0.

			// We need to know the next point to determine which way to turn.
			var np scaledPoint
			if i == len(points)-1 {
				np = sp
			} else {
				np = scale(points[i+1], scaleX, scaleY)
			}

			if pp.X == p.X {
				// If we're on the same vertical axis, our starting X coordinate is
				// the same as the control point coordinate
				sx = p.X

				// Offset start point from control point in the proper direction.
				if pp.Y < p.Y {
					sy = p.Y - 10
				} else {
					sy = p.Y + 10
				}

				ey = p.Y
				// Offset endpoint from control point in the proper direction.
				if np.X < p.X {
					ex = p.X - 10
				} else {
					ex = p.X + 10
				}
			} else if pp.Y == p.Y {
				// Horizontal decisions mirror vertical's above.
				sy = p.Y
				if pp.X < p.X {
					sx = p.X - 10
				} else {
					sx = p.X + 10
				}
				ex = p.X
				if np.Y <= p.Y {
					ey = p.Y - 10
				} else {
					ey = p.Y + 10
				}
			}

			fmt.Fprintf(&result, "L %g %g Q %g %g %g %g ", sx, sy, cx, cy, ex, ey)
		} else {
			// Just draw a straight line.
			fmt.Fprintf(&result, "L %g %g ", p.X, p.Y)
		}

		pp = p
	}
	return result.String()
}

Added 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
)

func TestCanvasToSVG(t *testing.T) {
	t.Parallel()
	data := []struct {
		input  []string
		length int
	}{
		// 0 Box with dashed corners and text
		{
			[]string{
				"+--.",
				"|Hi:",
				"+--+",
			},
			482,
		},

		// 2 Ticks and dots in lines.
		{
			[]string{
				" ------x----->",
				"",
				" <-----*------",
			},
			1084,
		},

		// 3 Just text
		{
			[]string{
				" foo",
			},
			261,
		},
	}
	for i, line := range data {
		canvas, err := newCanvas([]byte(strings.Join(line.input, "\n")))
		if err != nil {
			t.Fatalf("Error creating canvas: %s", err)
		}
		actual := string(canvasToSVG(canvas, "", 9, 16))
		// TODO(dhobsd): Use golden file? Worth postponing once output is actually
		// nice.
		if line.length != len(actual) {
			t.Errorf("%d: expected length %d, but got %d\n%q", i, line.length, len(actual), actual)
		}
	}
}

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
36







37
38
39
40

41
42

43
44
45

46

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
41
42
43
44
45
46

47
48

49
50
51
52
53

54
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package 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/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/runes"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:         "markdown",
		AltNames:     []string{"md"},
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxMarkdown,
		AltNames:      []string{meta.SyntaxMD},
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice {
	p := parseMarkdown(inp)
	return p.acceptBlockSlice(p.docNode)
	return p.acceptBlockChildren(p.docNode)
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	bs := parseBlocks(inp, nil, syntax)
	panic("markdown.parseInline not yet implemented")
	return bs.FirstParagraphInlines()
}

func parseMarkdown(inp *input.Input) *mdP {
	source := []byte(inp.Src[inp.Pos:])
	parser := gm.DefaultParser()
	node := parser.Parse(gmText.NewReader(source))
	textEnc := encoder.Create("text", nil)
	textEnc := textenc.Create()
	return &mdP{source: source, docNode: node, textEnc: textEnc}
}

type mdP struct {
	source  []byte
	docNode gmAst.Node
	textEnc encoder.Encoder
	textEnc *textenc.Encoder
}

func (p *mdP) acceptBlockSlice(docNode gmAst.Node) ast.BlockSlice {
func (p *mdP) acceptBlockChildren(docNode gmAst.Node) ast.BlockSlice {
	if docNode.Type() != gmAst.TypeDocument {
		panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type()))
	}
	result := make(ast.BlockSlice, 0, docNode.ChildCount())
	for child := docNode.FirstChild(); child != nil; child = child.NextSibling() {
		if block := p.acceptBlock(child); block != nil {
			result = append(result, block)
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
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







-
+















-
-
+
+
-
-







-
+




-
+







-
-
-
+
+
+




-
+

-
+


-
-
-
+
+
+



-
+

-
-
+
+









+
-
+
+
+






-
+







-
-
+
+

-
+

-
+











-
+

-
+














-
-
+
+
-
-





-
+

-
+



+
-
-
+
+
+
+

-
-
+
+



-
+







	case *gmAst.Paragraph:
		return p.acceptParagraph(n)
	case *gmAst.TextBlock:
		return p.acceptTextBlock(n)
	case *gmAst.Heading:
		return p.acceptHeading(n)
	case *gmAst.ThematicBreak:
		return p.acceptThematicBreak(n)
		return p.acceptThematicBreak()
	case *gmAst.CodeBlock:
		return p.acceptCodeBlock(n)
	case *gmAst.FencedCodeBlock:
		return p.acceptFencedCodeBlock(n)
	case *gmAst.Blockquote:
		return p.acceptBlockquote(n)
	case *gmAst.List:
		return p.acceptList(n)
	case *gmAst.HTMLBlock:
		return p.acceptHTMLBlock(n)
	}
	panic(fmt.Sprintf("Unhandled block node of kind %v", node.Kind()))
}

func (p *mdP) acceptParagraph(node *gmAst.Paragraph) ast.ItemNode {
	if ins := p.acceptInlineSlice(node); len(ins) > 0 {
		return &ast.ParaNode{
	if is := p.acceptInlineChildren(node); len(is) > 0 {
		return &ast.ParaNode{Inlines: is}
			Inlines: ins,
		}
	}
	return nil
}

func (p *mdP) acceptHeading(node *gmAst.Heading) *ast.HeadingNode {
	return &ast.HeadingNode{
		Level:   node.Level,
		Inlines: p.acceptInlineSlice(node),
		Inlines: p.acceptInlineChildren(node),
		Attrs:   nil,
	}
}

func (p *mdP) acceptThematicBreak(node *gmAst.ThematicBreak) *ast.HRuleNode {
func (*mdP) acceptThematicBreak() *ast.HRuleNode {
	return &ast.HRuleNode{
		Attrs: nil, //TODO
	}
}

func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode {
	return &ast.VerbatimNode{
		Code:  ast.VerbatimProg,
		Attrs: nil, //TODO
		Lines: p.acceptRawText(node),
		Kind:    ast.VerbatimProg,
		Attrs:   nil, //TODO
		Content: p.acceptRawText(node),
	}
}

func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode {
	var attrs *ast.Attributes
	var a attrs.Attributes
	if language := node.Language(p.source); len(language) > 0 {
		attrs = attrs.Set("class", "language-"+cleanText(string(language), true))
		a = a.Set("class", "language-"+cleanText(language, true))
	}
	return &ast.VerbatimNode{
		Code:  ast.VerbatimProg,
		Attrs: attrs,
		Lines: p.acceptRawText(node),
		Kind:    ast.VerbatimProg,
		Attrs:   a,
		Content: p.acceptRawText(node),
	}
}

func (p *mdP) acceptRawText(node gmAst.Node) []string {
func (p *mdP) acceptRawText(node gmAst.Node) []byte {
	lines := node.Lines()
	result := make([]string, 0, lines.Len())
	for i := 0; i < lines.Len(); i++ {
	result := make([]byte, 0, 512)
	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]
			}
		}
		if i > 0 {
		result = append(result, string(line))
			result = append(result, '\n')
		}
		result = append(result, line...)
	}
	return result
}

func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *ast.NestedListNode {
	return &ast.NestedListNode{
		Code: ast.NestedListQuote,
		Kind: ast.NestedListQuote,
		Items: []ast.ItemSlice{
			p.acceptItemSlice(node),
		},
	}
}

func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode {
	code := ast.NestedListUnordered
	var attrs *ast.Attributes
	kind := ast.NestedListUnordered
	var a attrs.Attributes
	if node.IsOrdered() {
		code = ast.NestedListOrdered
		kind = ast.NestedListOrdered
		if node.Start != 1 {
			attrs = attrs.Set("start", fmt.Sprintf("%d", node.Start))
			a = a.Set("start", strconv.Itoa(node.Start))
		}
	}
	items := make([]ast.ItemSlice, 0, node.ChildCount())
	for child := node.FirstChild(); child != nil; child = child.NextSibling() {
		item, ok := child.(*gmAst.ListItem)
		if !ok {
			panic(fmt.Sprintf("Expected list item node, but got %v", child.Kind()))
		}
		items = append(items, p.acceptItemSlice(item))
	}
	return &ast.NestedListNode{
		Code:  code,
		Kind:  kind,
		Items: items,
		Attrs: attrs,
		Attrs: a,
	}
}

func (p *mdP) acceptItemSlice(node gmAst.Node) ast.ItemSlice {
	result := make(ast.ItemSlice, 0, node.ChildCount())
	for elem := node.FirstChild(); elem != nil; elem = elem.NextSibling() {
		if item := p.acceptBlock(elem); item != nil {
			result = append(result, item)
		}
	}
	return result
}

func (p *mdP) acceptTextBlock(node *gmAst.TextBlock) ast.ItemNode {
	if ins := p.acceptInlineSlice(node); len(ins) > 0 {
		return &ast.ParaNode{
	if is := p.acceptInlineChildren(node); len(is) > 0 {
		return &ast.ParaNode{Inlines: is}
			Inlines: ins,
		}
	}
	return nil
}

func (p *mdP) acceptHTMLBlock(node *gmAst.HTMLBlock) *ast.VerbatimNode {
	lines := p.acceptRawText(node)
	content := p.acceptRawText(node)
	if node.HasClosure() {
		closure := string(node.ClosureLine.Value(p.source))
		closure := node.ClosureLine.Value(p.source)
		if l := len(closure); l > 1 && closure[l-1] == '\n' {
			closure = closure[:l-1]
		}
		if len(content) > 1 {
		lines = append(lines, closure)
	}
			content = append(content, '\n')
		}
		content = append(content, closure...)
	}
	return &ast.VerbatimNode{
		Code:  ast.VerbatimHTML,
		Lines: lines,
		Kind:    ast.VerbatimHTML,
		Content: content,
	}
}

func (p *mdP) acceptInlineSlice(node gmAst.Node) ast.InlineSlice {
func (p *mdP) acceptInlineChildren(node gmAst.Node) ast.InlineSlice {
	result := make(ast.InlineSlice, 0, node.ChildCount())
	for child := node.FirstChild(); child != nil; child = child.NextSibling() {
		if inlines := p.acceptInline(child); inlines != nil {
			result = append(result, inlines...)
		}
	}
	return result
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
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







-
+














-
+






-
+







	if node.IsRaw() {
		return splitText(string(segment.Value(p.source)))
	}
	ins := splitText(string(segment.Value(p.source)))
	result := make(ast.InlineSlice, 0, len(ins)+1)
	for _, in := range ins {
		if tn, ok := in.(*ast.TextNode); ok {
			tn.Text = cleanText(tn.Text, true)
			tn.Text = cleanText([]byte(tn.Text), true)
		}
		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 ast.InlineSlice{}
		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 runes.IsSpace(ch) {
		if input.IsSpace(ch) {
			if state == 1 {
				result = append(result, &ast.TextNode{Text: text[lastPos:pos]})
				lastPos = pos
			}
			state = 2
		} else {
			if state == 2 {
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
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







-
-
-
-
+
+
+
+
-
-
-
+



-
+







-
+

-
+





-
-
-
-
-
-
+
+
+
+
+
+
+
-
-


-
+







-
-
-
+
+
+




-
-
-
+
+
+


-
+


-
+

-
+




-
-
+
+
-
-
-



-
+

-
+



-
+

-
+





-
-
-
-
+
+
+
+




-
+
-
-
+





-
-
-
-
+
+
+
+


-
+


-
+





-
+

-
+



-
+
-


-
+
-
-
-
-



-
+

-
-
+
+
-
-
-
-
-



-
-
+
+
-
-
+





-
-
+
+

-
+



-
-
-
+
+
+



		result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:]})
	default:
		panic(fmt.Sprintf("Unexpected state %v", state))
	}
	return result
}

var ignoreAfterBS = map[byte]bool{
	'!': true, '"': true, '#': true, '$': true, '%': true, '&': true,
	'\'': true, '(': true, ')': true, '*': true, '+': true, ',': true,
	'-': true, '.': true, '/': true, ':': true, ';': true, '<': true,
var ignoreAfterBS = map[byte]struct{}{
	'!': {}, '"': {}, '#': {}, '$': {}, '%': {}, '&': {}, '\'': {}, '(': {},
	')': {}, '*': {}, '+': {}, ',': {}, '-': {}, '.': {}, '/': {}, ':': {},
	';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {},
	'=': true, '>': true, '?': true, '@': true, '[': true, '\\': true,
	']': true, '^': true, '_': true, '`': true, '{': true, '|': true,
	'}': true, '~': true,
	']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {},
}

// cleanText removes backslashes from TextNodes and expands entities
func cleanText(text string, cleanBS bool) string {
func cleanText(text []byte, cleanBS bool) string {
	lastPos := 0
	var sb strings.Builder
	for pos, ch := range text {
		if pos < lastPos {
			continue
		}
		if ch == '&' {
			inp := input.NewInput(text[pos:])
			inp := input.NewInput([]byte(text[pos:]))
			if s, ok := inp.ScanEntity(); ok {
				sb.WriteString(text[lastPos:pos])
				sb.Write(text[lastPos:pos])
				sb.WriteString(s)
				lastPos = pos + inp.Pos
			}
			continue
		}
		if cleanBS && ch == '\\' && pos < len(text)-1 && ignoreAfterBS[text[pos+1]] {
			sb.WriteString(text[lastPos:pos])
			sb.WriteByte(text[pos+1])
			lastPos = pos + 2
		}
	}
		if cleanBS && ch == '\\' && pos < len(text)-1 {
			if _, found := ignoreAfterBS[text[pos+1]]; found {
				sb.Write(text[lastPos:pos])
				sb.WriteByte(text[pos+1])
				lastPos = pos + 2
			}
		}
	if lastPos == 0 {
		return text
	}
	if lastPos < len(text) {
		sb.WriteString(text[lastPos:])
		sb.Write(text[lastPos:])
	}
	return sb.String()
}

func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice {
	return ast.InlineSlice{
		&ast.LiteralNode{
			Code:  ast.LiteralProg,
			Attrs: nil, //TODO
			Text:  cleanCodeSpan(string(node.Text(p.source))),
			Kind:    ast.LiteralProg,
			Attrs:   nil, //TODO
			Content: cleanCodeSpan(node.Text(p.source)),
		},
	}
}

func cleanCodeSpan(text string) string {
	if text == "" {
		return ""
func cleanCodeSpan(text []byte) []byte {
	if len(text) == 0 {
		return nil
	}
	lastPos := 0
	var sb strings.Builder
	var buf bytes.Buffer
	for pos, ch := range text {
		if ch == '\n' {
			sb.WriteString(text[lastPos:pos])
			buf.Write(text[lastPos:pos])
			if pos < len(text)-1 {
				sb.WriteByte(' ')
				buf.WriteByte(' ')
			}
			lastPos = pos + 1
		}
	}
	if lastPos == 0 {
		return text
	buf.Write(text[lastPos:])
	return buf.Bytes()
	}
	sb.WriteString(text[lastPos:])
	return sb.String()
}

func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice {
	code := ast.FormatEmph
	kind := ast.FormatEmph
	if node.Level == 2 {
		code = ast.FormatStrong
		kind = ast.FormatStrong
	}
	return ast.InlineSlice{
		&ast.FormatNode{
			Code:    code,
			Kind:    kind,
			Attrs:   nil, //TODO
			Inlines: p.acceptInlineSlice(node),
			Inlines: p.acceptInlineChildren(node),
		},
	}
}

func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice {
	ref := ast.ParseReference(cleanText(string(node.Destination), true))
	var attrs *ast.Attributes
	if title := string(node.Title); len(title) > 0 {
		attrs = attrs.Set("title", cleanText(title, true))
	ref := ast.ParseReference(cleanText(node.Destination, true))
	var a attrs.Attributes
	if title := node.Title; len(title) > 0 {
		a = a.Set("title", cleanText(title, true))
	}
	return ast.InlineSlice{
		&ast.LinkNode{
			Ref:     ref,
			Inlines: p.acceptInlineSlice(node),
			Inlines: p.acceptInlineChildren(node),
			OnlyRef: false,
			Attrs:   attrs,
			Attrs:   a,
		},
	}
}

func (p *mdP) acceptImage(node *gmAst.Image) ast.InlineSlice {
	ref := ast.ParseReference(cleanText(string(node.Destination), true))
	var attrs *ast.Attributes
	if title := string(node.Title); len(title) > 0 {
		attrs = attrs.Set("title", cleanText(title, true))
	ref := ast.ParseReference(cleanText(node.Destination, true))
	var a attrs.Attributes
	if title := node.Title; len(title) > 0 {
		a = a.Set("title", cleanText(title, true))
	}
	return ast.InlineSlice{
		&ast.ImageNode{
		&ast.EmbedRefNode{
			Ref:     ref,
			Inlines: p.flattenInlineSlice(node),
			Attrs:   attrs,
			Attrs:   a,
		},
	}
}

func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice {
	ins := p.acceptInlineSlice(node)
	is := p.acceptInlineChildren(node)
	var sb strings.Builder
	_, err := p.textEnc.WriteInlines(&sb, ins)
	_, err := p.textEnc.WriteInlines(&sb, &is)
	if err != nil {
		panic(err)
	}
	text := sb.String()
	if sb.Len() == 0 {
	if text == "" {
		return nil
	}
	return ast.InlineSlice{
	return ast.InlineSlice{&ast.TextNode{Text: sb.String()}}
		&ast.TextNode{
			Text: text,
		},
	}
}

func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice {
	url := node.URL(p.source)
	u := node.URL(p.source)
	if node.AutoLinkType == gmAst.AutoLinkEmail &&
		!bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
		url = append([]byte("mailto:"), url...)
		!bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) {
		u = append([]byte("mailto:"), u...)
	}
	ref := ast.ParseReference(cleanText(string(url), false))
	label := node.Label(p.source)
	if len(label) == 0 {
		label = url
	}
	return ast.InlineSlice{
		&ast.LinkNode{
			Ref:     ref,
			Inlines: ast.InlineSlice{&ast.TextNode{Text: string(label)}},
			Ref:     ast.ParseReference(cleanText(u, false)),
			Inlines: nil,
			OnlyRef: true,
			Attrs:   nil, //TODO
			Attrs:   nil, // TODO
		},
	}
}

func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice {
	segs := make([]string, 0, node.Segments.Len())
	for i := 0; i < node.Segments.Len(); i++ {
	segs := make([][]byte, 0, node.Segments.Len())
	for i := range node.Segments.Len() {
		segment := node.Segments.At(i)
		segs = append(segs, string(segment.Value(p.source)))
		segs = append(segs, segment.Value(p.source))
	}
	return ast.InlineSlice{
		&ast.LiteralNode{
			Code:  ast.LiteralHTML,
			Attrs: nil, // TODO: add HTML as language
			Text:  strings.Join(segs, ""),
			Kind:    ast.LiteralHTML,
			Attrs:   nil, // TODO: add HTML as language
			Content: bytes.Join(segs, nil),
		},
	}
}

Changes to 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
1

2
3

4
5
6
7
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 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package markdown provides a parser for markdown.
package markdown

import (
	"strings"
	"testing"

	"zettelstore.de/z/ast"
)

func TestSplitText(t *testing.T) {
	t.Parallel()
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"abc", "Tabc"},
		{" ", "S "},

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
26







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

-
+

-
+




+
+
+


-
+



-
-
-
-
+
+
+
+




-
-
-
-
+
+
+
+
+
+
+


-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+

-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package none provides a none-parser for meta data.
// 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"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:         meta.ValueSyntaxNone,
		AltNames:     []string{},
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxNone,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
	descrlist := &ast.DescriptionListNode{}
	for _, p := range m.Pairs(true) {
		descrlist.Descriptions = append(
			descrlist.Descriptions, getDescription(p.Key, p.Value))
	}
	return ast.BlockSlice{descrlist}
}

func getDescription(key, value string) ast.Description {
	return ast.Description{
		Term: ast.InlineSlice{&ast.TextNode{Text: key}},
		Descriptions: []ast.DescriptionSlice{
			ast.DescriptionSlice{
				&ast.ParaNode{
					Inlines: convertToInlineSlice(value, meta.Type(key)),
				},
			},
		},
	}
}

func convertToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice {
	var sliceData []string
	if dt.IsSet {
		sliceData = meta.ListFromValue(value)
		if len(sliceData) == 0 {
			return ast.InlineSlice{}
		}
	} else {
		sliceData = []string{value}
	}
	var makeLink bool
	switch dt {
	case meta.TypeID, meta.TypeIDSet:
		makeLink = true
	}


func parseBlocks(*input.Input, *meta.Meta, string) ast.BlockSlice { return nil }
	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.SpaceNode{Lexeme: " "})
		}

		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
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{
	return nil
		&ast.FormatNode{
			Code:  ast.FormatSpan,
			Attrs: &ast.Attributes{Attrs: map[string]string{"class": "warning"}},
			Inlines: ast.InlineSlice{
				&ast.TextNode{Text: "parser.meta.ParseInlines:"},
				&ast.SpaceNode{Lexeme: " "},
				&ast.TextNode{Text: "not"},
				&ast.SpaceNode{Lexeme: " "},
				&ast.TextNode{Text: "possible"},
				&ast.SpaceNode{Lexeme: " "},
				&ast.TextNode{Text: "("},
				&ast.TextNode{Text: inp.Src[0:inp.Pos]},
				&ast.TextNode{Text: ")"},
			},
		},
	}
}

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
32
33
34







35
36
37
38
39
40

41
42

43
44
45
46
47

48
49
50








51

52
53
54
55
56
57
58
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
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package parser provides a generic interface to a range of different parsers.
package parser

import (
	"context"
	"log"
	"fmt"
	"strings"

	"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/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)

// 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.
type Info struct {
	Name         string
	AltNames     []string
	ParseBlocks  func(*input.Input, *meta.Meta, string) ast.BlockSlice
	ParseInlines func(*input.Input, string) ast.InlineSlice
	Name          string
	AltNames      []string
	IsASTParser   bool
	IsTextFormat  bool
	IsImageFormat bool
	ParseBlocks   func(*input.Input, *meta.Meta, string) ast.BlockSlice
	ParseInlines  func(*input.Input, string) ast.InlineSlice
}

var registry = map[string]*Info{}

// Register the parser (info) for later retrieval.
func Register(pi *Info) *Info {
func Register(pi *Info) {
	if _, ok := registry[pi.Name]; ok {
		log.Fatalf("Parser %q already registered", pi.Name)
		panic(fmt.Sprintf("Parser %q already registered", pi.Name))
	}
	registry[pi.Name] = pi
	for _, alt := range pi.AltNames {
		if _, ok := registry[alt]; ok {
			log.Fatalf("Parser %q already registered", alt)
			panic(fmt.Sprintf("Parser %q already registered", alt))
		}
		registry[alt] = pi
	}
}

// GetSyntaxes returns a list of syntaxes implemented by all registered parsers.
func GetSyntaxes() []string {
	result := make([]string, 0, len(registry))
	for syntax := range registry {
		result = append(result, syntax)
	}
	return pi
	return result
}

// Get the parser (info) by name. If name not found, use a default parser.
func Get(name string) *Info {
	if pi := registry[name]; pi != nil {
		return pi
	}
	if pi := registry["plain"]; pi != nil {
		return pi
	}
	log.Printf("No parser for %q found", name)
	panic("No default parser registered")
	panic(fmt.Sprintf("No parser for %q found", name))
}

// IsASTParser returns whether the given syntax parses text into an AST or not.
func IsASTParser(syntax string) bool {
	pi, ok := registry[syntax]
	if !ok {
		return false
	}
	return pi.IsASTParser
}

// 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) ast.BlockSlice {
func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice {
	bs := Get(syntax).ParseBlocks(inp, m, syntax)
	cleaner.CleanupBlockSlice(bs)
	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(title string) ast.InlineSlice {
	return ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk)
func ParseMetadata(value string) ast.InlineSlice {
	return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk)
}

// 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 {
	return ast.CreateInlineSliceFromWords(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
	}
	if descr, found := m.Get(api.KeySummary); found {
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return ast.CreateInlineSliceFromWords("Zettel", "without", "title:", m.Zid.String())
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(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 := rtConfig.AddDefaultValues(m)
	inhMeta := m
	if rtConfig != nil {
		inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta)
	}
	if syntax == "" {
		syntax, _ = inhMeta.Get(meta.KeySyntax)
		syntax = inhMeta.GetDefault(api.KeySyntax, meta.DefaultSyntax)
	}
	parseMeta := inhMeta
	if syntax == meta.ValueSyntaxNone {
	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:     ParseBlocks(input.NewInput(zettel.Content.AsString()), parseMeta, syntax),
		Ast:     ParseBlocks(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi),
		Syntax:  syntax,
	}
}

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/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
		image  bool
	}{
		{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.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.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
29
















30
31
32
33
34







35
36
37
38
39







40
41
42
43
44







45
46
47
48







49
50
51
52
53



54
55
56
57
58
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



























1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20



21
22
23
24
25

26
27
28
29
30




31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48



49
50
51
52
53
54
55
56
57



58
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

-
+

-
+




+
+
+






+


-
-
-
+
+
+
+

-
+




-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
-
+
+
+
+
+
+
+


-
-
-
+
+
+
+
+
+
+


-
-
-
+
+
+
+
+
+
+



-
+
+
+
+
+
+
+


-
-
-
+
+
+



-
-
-
-
-
-
-
-
-
-
-
+
+
+

+
+
-
-
+
+

-
+
-
-
-
-
+
+
+
-
-
+


-
-
-
+
+
+


-
+
-
-
-
-







-
+
-
-
-
+
+
-
-
+



-
+


-
+






+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package plain provides a parser for plain text data.
package plain

import (
	"bytes"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/runes"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:         "txt",
		AltNames:     []string{"plain", "text"},
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxTxt,
		AltNames:      []string{meta.SyntaxPlain, meta.SyntaxText},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.SyntaxHTML,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocksHTML,
		ParseInlines:  parseInlinesHTML,
	})
	parser.Register(&parser.Info{
		Name:         "css",
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxCSS,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:         "svg",
		ParseBlocks:  parseSVGBlocks,
		ParseInlines: parseSVGInlines,
		Name:          meta.SyntaxSVG,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: true,
		ParseBlocks:   parseSVGBlocks,
		ParseInlines:  parseSVGInlines,
	})
	parser.Register(&parser.Info{
		Name:         "mustache",
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxSxn,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseSxnBlocks,
		ParseInlines:  parseSxnInlines,
	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
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 {
	return doParseBlocks(inp, syntax, ast.VerbatimHTML)
}
func doParseBlocks(inp *input.Input, syntax string, kind ast.VerbatimKind) ast.BlockSlice {
	return ast.BlockSlice{
		&ast.VerbatimNode{
			Code:  ast.VerbatimProg,
			Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}},
			Lines: readLines(inp),
			Kind:    kind,
			Attrs:   attrs.Attributes{"": syntax},
			Content: inp.ScanLineContent(),
		},
	}
}

func readLines(inp *input.Input) (lines []string) {
	for {
		inp.EatEOL()
		posL := inp.Pos
		if inp.Ch == input.EOS {
			return lines
		}
		inp.SkipToEOL()
		lines = append(lines, inp.Src[posL:inp.Pos])
	}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	return doParseInlines(inp, syntax, ast.LiteralProg)
}
func parseInlinesHTML(inp *input.Input, syntax string) ast.InlineSlice {
	return doParseInlines(inp, syntax, ast.LiteralHTML)

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
}
func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{
	return ast.InlineSlice{&ast.LiteralNode{
		&ast.LiteralNode{
			Code:  ast.LiteralProg,
			Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}},
			Text:  inp.Src[0:inp.Pos],
		Kind:    kind,
		Attrs:   attrs.Attributes{"": syntax},
		Content: append([]byte(nil), inp.Src[0:inp.Pos]...),
		},
	}
	}}
}

func parseSVGBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
	ins := parseSVGInlines(inp, syntax)
	if ins == nil {
func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	is := parseSVGInlines(inp, syntax)
	if len(is) == 0 {
		return nil
	}
	return ast.BlockSlice{
	return ast.BlockSlice{ast.CreateParaNode(is...)}
		&ast.ParaNode{
			Inlines: ins,
		},
	}
}

func parseSVGInlines(inp *input.Input, syntax string) ast.InlineSlice {
	svgSrc := scanSVG(inp)
	if svgSrc == "" {
		return nil
	}
	return ast.InlineSlice{
	return ast.InlineSlice{&ast.EmbedBLOBNode{
		&ast.ImageNode{
			Blob:   []byte(svgSrc),
			Syntax: syntax,
		Blob:   []byte(svgSrc),
		Syntax: syntax,
		},
	}
	}}
}

func scanSVG(inp *input.Input) string {
	for runes.IsSpace(inp.Ch) {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	}
	svgSrc := inp.Src[inp.Pos:]
	svgSrc := string(inp.Src[inp.Pos:])
	if !strings.HasPrefix(svgSrc, "<svg ") {
		return ""
	}
	// TODO: check proper end </svg>
	return svgSrc
}

func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
	_, 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]...),
	}}
}

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
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
48

-
+

-
+




+
+
+


-





-
-
+
+






-
+
+



-
+








-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"fmt"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
)

// parseBlockSlice parses a sequence of blocks.
func (cp *zmkP) parseBlockSlice() ast.BlockSlice {
	inp := cp.inp
	var lastPara *ast.ParaNode
	result := make(ast.BlockSlice, 0, 2)
	bs := ast.BlockSlice{}

	for inp.Ch != input.EOS {
		bn, cont := cp.parseBlock(lastPara)
		if bn != nil {
			result = append(result, bn)
			bs = append(bs, bn)
		}
		if !cont {
			lastPara, _ = bn.(*ast.ParaNode)
		}
	}
	if cp.nestingLevel != 0 {
		panic("Nesting level was not decremented")
	}
	return result
	return bs
}

// parseBlock parses one block.
func (cp *zmkP) parseBlock(lastPara *ast.ParaNode) (res ast.BlockNode, cont bool) {
	inp := cp.inp
	pos := inp.Pos
	if cp.nestingLevel <= maxNestingLevel {
54
55
56
57
58
59
60
61

62
63
64
65
66
67
68
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71







-
+







			return nil, false
		case '\n', '\r':
			inp.EatEOL()
			cp.cleanupListsAfterEOL()
			return nil, false
		case ':':
			bn, success = cp.parseColon()
		case '`', runeModGrave, '%':
		case '@', '`', runeModGrave, '%', '~', '$':
			cp.clearStacked()
			bn, success = cp.parseVerbatim()
		case '"', '<':
			cp.clearStacked()
			bn, success = cp.parseRegion()
		case '=':
			cp.clearStacked()
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
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







-
+



-
+
+
+
+









+
+
-
+





+
+
+
+
+
+
+
+
+
+







			bn, success = cp.parseNestedList()
		case ';':
			cp.lists = nil
			cp.table = nil
			bn, success = cp.parseDefTerm()
		case ' ':
			cp.table = nil
			bn, success = cp.parseIndent()
			bn, success = nil, cp.parseIndent()
		case '|':
			cp.lists = nil
			cp.descrl = nil
			bn, success = cp.parseRow()
			bn, success = cp.parseRow(), true
		case '{':
			cp.clearStacked()
			bn, success = cp.parseTransclusion()
		}

		if success {
			return bn, false
		}
	}
	inp.SetPos(pos)
	cp.clearStacked()
	pn := cp.parsePara()
	if startsWithSpaceSoftBreak(pn) {
		pn.Inlines = pn.Inlines[2:]
	if lastPara != nil {
	} else if lastPara != nil {
		lastPara.Inlines = append(lastPara.Inlines, pn.Inlines...)
		return nil, true
	}
	return pn, false
}

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 isSpace && isBreak
}

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{})
		}
	}
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
142
143
144
145
146
147
148

149
150
151
152

153
154

155
156
157
158
159


160
161
162
163
164
165
166
167
168







-
+



-
+

-
+




-
-
+
+







		return cp.parseRegion()
	}
	return cp.parseDefDescr()
}

// parsePara parses paragraphed inline material.
func (cp *zmkP) parsePara() *ast.ParaNode {
	pn := &ast.ParaNode{}
	ins := ast.InlineSlice{}
	for {
		in := cp.parseInline()
		if in == nil {
			return pn
			return &ast.ParaNode{Inlines: ins}
		}
		pn.Inlines = append(pn.Inlines, in)
		ins = append(ins, in)
		if _, ok := in.(*ast.BreakNode); ok {
			ch := cp.inp.Ch
			switch ch {
			// Must contain all cases from above switch in parseBlock.
			case input.EOS, '\n', '\r', '`', runeModGrave, '%', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|':
				return pn
			case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '~', '$', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{':
				return &ast.ParaNode{Inlines: ins}
			}
		}
	}
}

// countDelim read from input until a non-delimiter is found and returns number of delimiter chars.
func (cp *zmkP) countDelim(delim rune) int {
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
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







-
+




-
+

+
+

-
+

-
+
+
+
+
+



-
+














+
+
+
-
+



-
+









-
+







-
+




-
+
+
+
+
+
+







func (cp *zmkP) parseVerbatim() (rn *ast.VerbatimNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	cnt := cp.countDelim(fch)
	if cnt < 3 {
		return nil, false
	}
	attrs := cp.parseAttributes(true)
	attrs := cp.parseBlockAttributes()
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	var code ast.VerbatimCode
	var kind ast.VerbatimKind
	switch fch {
	case '@':
		kind = ast.VerbatimZettel
	case '`', runeModGrave:
		code = ast.VerbatimProg
		kind = ast.VerbatimProg
	case '%':
		code = ast.VerbatimComment
		kind = ast.VerbatimComment
	case '~':
		kind = ast.VerbatimEval
	case '$':
		kind = ast.VerbatimMath
	default:
		panic(fmt.Sprintf("%q is not a verbatim char", fch))
	}
	rn = &ast.VerbatimNode{Code: code, Attrs: attrs}
	rn = &ast.VerbatimNode{Kind: kind, Attrs: attrs, Content: make([]byte, 0, 512)}
	for {
		inp.EatEOL()
		posL := inp.Pos
		switch inp.Ch {
		case fch:
			if cp.countDelim(fch) >= cnt {
				inp.SkipToEOL()
				return rn, true
			}
			inp.SetPos(posL)
		case input.EOS:
			return nil, false
		}
		inp.SkipToEOL()
		if len(rn.Content) > 0 {
			rn.Content = append(rn.Content, '\n')
		}
		rn.Lines = append(rn.Lines, inp.Src[posL:inp.Pos])
		rn.Content = append(rn.Content, inp.Src[posL:inp.Pos]...)
	}
}

var runeRegion = map[rune]ast.RegionCode{
var runeRegion = map[rune]ast.RegionKind{
	':': ast.RegionSpan,
	'<': ast.RegionQuote,
	'"': ast.RegionVerse,
}

// parseRegion parses a block region.
func (cp *zmkP) parseRegion() (rn *ast.RegionNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	code, ok := runeRegion[fch]
	kind, ok := runeRegion[fch]
	if !ok {
		panic(fmt.Sprintf("%q is not a region char", fch))
	}
	cnt := cp.countDelim(fch)
	if cnt < 3 {
		return nil, false
	}
	attrs := cp.parseAttributes(true)
	attrs := cp.parseBlockAttributes()
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	rn = &ast.RegionNode{Code: code, Attrs: attrs}
	rn = &ast.RegionNode{
		Kind:    kind,
		Attrs:   attrs,
		Blocks:  nil,
		Inlines: nil,
	}
	var lastPara *ast.ParaNode
	inp.EatEOL()
	for {
		posL := inp.Pos
		switch inp.Ch {
		case fch:
			if cp.countDelim(fch) >= cnt {
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
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







-





-
-
+
+


-
-
-





+
+
+
-
+








+
-
-
+
+




-









-
+




-
+








-
-
+
+



-
+



-
-
+
+

-
-
+
+
+
+
+
+



-
+

-
+


















-
-
+
+

-
-
+
+







-
+









-
+







		}
		in := cp.parseInline()
		if in == nil {
			return
		}
		rn.Inlines = append(rn.Inlines, in)
	}

}

// parseHeading parses a head line.
func (cp *zmkP) parseHeading() (hn *ast.HeadingNode, success bool) {
	inp := cp.inp
	lvl := cp.countDelim(inp.Ch)
	if lvl < 3 {
	delims := cp.countDelim(inp.Ch)
	if delims < 3 {
		return nil, false
	}
	if lvl > 7 {
		lvl = 7
	}
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	if delims > 7 {
		delims = 7
	}
	hn = &ast.HeadingNode{Level: lvl - 1}
	hn = &ast.HeadingNode{Level: delims - 2, Inlines: nil}
	for {
		if input.IsEOLEOS(inp.Ch) {
			return hn, true
		}
		in := cp.parseInline()
		if in == nil {
			return hn, true
		}
		hn.Inlines = append(hn.Inlines, in)
		if inp.Ch == '{' {
			attrs := cp.parseAttributes(true)
		if inp.Ch == '{' && inp.Peek() != '{' {
			attrs := cp.parseBlockAttributes()
			hn.Attrs = attrs
			inp.SkipToEOL()
			return hn, true
		}
		hn.Inlines = append(hn.Inlines, in)
	}
}

// parseHRule parses a horizontal rule.
func (cp *zmkP) parseHRule() (hn *ast.HRuleNode, success bool) {
	inp := cp.inp
	if cp.countDelim(inp.Ch) < 3 {
		return nil, false
	}
	attrs := cp.parseAttributes(true)
	attrs := cp.parseBlockAttributes()
	inp.SkipToEOL()
	return &ast.HRuleNode{Attrs: attrs}, true
}

var mapRuneNestedList = map[rune]ast.NestedListCode{
var mapRuneNestedList = map[rune]ast.NestedListKind{
	'*': ast.NestedListUnordered,
	'#': ast.NestedListOrdered,
	'>': ast.NestedListQuote,
}

// parseNestedList parses a list.
func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) {
	inp := cp.inp
	codes := cp.parseNestedListCodes()
	if codes == nil {
	kinds := cp.parseNestedListKinds()
	if kinds == nil {
		return nil, false
	}
	cp.skipSpace()
	if codes[len(codes)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) {
	if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) {
		return nil, false
	}

	if len(codes) < len(cp.lists) {
		cp.lists = cp.lists[:len(codes)]
	if len(kinds) < len(cp.lists) {
		cp.lists = cp.lists[:len(kinds)]
	}
	ln, newLnCount := cp.buildNestedList(codes)
	ln.Items = append(ln.Items, ast.ItemSlice{cp.parseLinePara()})
	ln, newLnCount := cp.buildNestedList(kinds)
	pn := cp.parseLinePara()
	if pn == nil {
		pn = &ast.ParaNode{}
	}
	ln.Items = append(ln.Items, ast.ItemSlice{pn})
	return cp.cleanupParsedNestedList(newLnCount)
}

func (cp *zmkP) parseNestedListCodes() []ast.NestedListCode {
func (cp *zmkP) parseNestedListKinds() []ast.NestedListKind {
	inp := cp.inp
	codes := make([]ast.NestedListCode, 0, 4)
	codes := make([]ast.NestedListKind, 0, 4)
	for {
		code, ok := mapRuneNestedList[inp.Ch]
		if !ok {
			panic(fmt.Sprintf("%q is not a region char", inp.Ch))
		}
		codes = append(codes, code)
		inp.Next()
		switch inp.Ch {
		case '*', '#', '>':
		case ' ', input.EOS, '\n', '\r':
			return codes
		default:
			return nil
		}
	}

}

func (cp *zmkP) buildNestedList(codes []ast.NestedListCode) (ln *ast.NestedListNode, newLnCount int) {
	for i, code := range codes {
func (cp *zmkP) buildNestedList(kinds []ast.NestedListKind) (ln *ast.NestedListNode, newLnCount int) {
	for i, kind := range kinds {
		if i < len(cp.lists) {
			if cp.lists[i].Code != code {
				ln = &ast.NestedListNode{Code: code}
			if cp.lists[i].Kind != kind {
				ln = &ast.NestedListNode{Kind: kind}
				newLnCount++
				cp.lists[i] = ln
				cp.lists = cp.lists[:i+1]
			} else {
				ln = cp.lists[i]
			}
		} else {
			ln = &ast.NestedListNode{Code: code}
			ln = &ast.NestedListNode{Kind: kind}
			newLnCount++
			cp.lists = append(cp.lists, ln)
		}
	}
	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
411
412
413
414
415
416
417
418

419
420
421
422
423
424
425
446
447
448
449
450
451
452

453
454
455
456
457
458
459
460







-
+







	defPos := len(descrl.Descriptions) - 1
	if defPos == 0 {
		res = descrl
	}
	for {
		in := cp.parseInline()
		if in == nil {
			if descrl.Descriptions[defPos].Term == nil {
			if len(descrl.Descriptions[defPos].Term) == 0 {
				return nil, false
			}
			return res, true
		}
		descrl.Descriptions[defPos].Term = append(descrl.Descriptions[defPos].Term, in)
		if _, ok := in.(*ast.BreakNode); ok {
			return res, true
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
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







-
+













-
+










-
+


-
+

-
+












+
+
+







	inp.Next()
	cp.skipSpace()
	descrl := cp.descrl
	if descrl == nil || len(descrl.Descriptions) == 0 {
		return nil, false
	}
	defPos := len(descrl.Descriptions) - 1
	if descrl.Descriptions[defPos].Term == nil {
	if len(descrl.Descriptions[defPos].Term) == 0 {
		return nil, false
	}
	pn := cp.parseLinePara()
	if pn == nil {
		return nil, false
	}
	cp.lists = nil
	cp.table = nil
	descrl.Descriptions[defPos].Descriptions = append(descrl.Descriptions[defPos].Descriptions, ast.DescriptionSlice{pn})
	return nil, true
}

// parseIndent parses initial spaces to continue a list.
func (cp *zmkP) parseIndent() (res ast.BlockNode, success bool) {
func (cp *zmkP) parseIndent() bool {
	inp := cp.inp
	cnt := 0
	for {
		inp.Next()
		if inp.Ch != ' ' {
			break
		}
		cnt++
	}
	if cp.lists != nil {
		return nil, cp.parseIndentForList(cnt)
		return cp.parseIndentForList(cnt)
	}
	if cp.descrl != nil {
		return nil, cp.parseIndentForDescription(cnt)
		return cp.parseIndentForDescription(cnt)
	}
	return nil, false
	return false
}

func (cp *zmkP) parseIndentForList(cnt int) bool {
	if len(cp.lists) < cnt {
		cnt = len(cp.lists)
	}
	cp.lists = cp.lists[:cnt]
	if cnt == 0 {
		return false
	}
	ln := cp.lists[cnt-1]
	pn := cp.parseLinePara()
	if pn == nil {
		pn = &ast.ParaNode{}
	}
	lbn := ln.Items[len(ln.Items)-1]
	if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
		lpn.Inlines = append(lpn.Inlines, pn.Inlines...)
	} else {
		ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn)
	}
	return true
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















































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







-
+







-
+



-
+


-
+

-
+

-
+





-
+



-
+
















-
+


-
+








-
+


-
+


-
+


-
+

-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
		return false
	}
	descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
	lbn := cp.descrl.Descriptions[defPos].Descriptions[descrPos]
	if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
		lpn.Inlines = append(lpn.Inlines, pn.Inlines...)
	} else {
		descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
		descrPos = len(cp.descrl.Descriptions[defPos].Descriptions) - 1
		cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn)
	}
	return true
}

// parseLinePara parses one line of inline material.
func (cp *zmkP) parseLinePara() *ast.ParaNode {
	pn := &ast.ParaNode{}
	ins := ast.InlineSlice{}
	for {
		in := cp.parseInline()
		if in == nil {
			if pn.Inlines == nil {
			if len(ins) == 0 {
				return nil
			}
			return pn
			return &ast.ParaNode{Inlines: ins}
		}
		pn.Inlines = append(pn.Inlines, in)
		ins = append(ins, in)
		if _, ok := in.(*ast.BreakNode); ok {
			return pn
			return &ast.ParaNode{Inlines: ins}
		}
	}
}

// parseRow parse one table row.
func (cp *zmkP) parseRow() (res ast.BlockNode, success bool) {
func (cp *zmkP) parseRow() ast.BlockNode {
	inp := cp.inp
	if inp.Peek() == '%' {
		inp.SkipToEOL()
		return nil, true
		return nil
	}
	row := ast.TableRow{}
	for {
		inp.Next()
		cell := cp.parseCell()
		if cell != nil {
			row = append(row, cell)
		}
		switch inp.Ch {
		case '\n', '\r':
			inp.EatEOL()
			fallthrough
		case input.EOS:
			// add to table
			if cp.table == nil {
				cp.table = &ast.TableNode{Rows: []ast.TableRow{row}}
				return cp.table, true
				return cp.table
			}
			cp.table.Rows = append(cp.table.Rows, row)
			return nil, true
			return nil
		}
		// inp.Ch must be '|'
	}
}

// parseCell parses one single cell of a table row.
func (cp *zmkP) parseCell() *ast.TableCell {
	inp := cp.inp
	var slice ast.InlineSlice
	var l ast.InlineSlice
	for {
		if input.IsEOLEOS(inp.Ch) {
			if len(slice) == 0 {
			if len(l) == 0 {
				return nil
			}
			return &ast.TableCell{Inlines: slice}
			return &ast.TableCell{Inlines: l}
		}
		if inp.Ch == '|' {
			return &ast.TableCell{Inlines: slice}
			return &ast.TableCell{Inlines: l}
		}
		slice = append(slice, cp.parseInline())
		l = append(l, cp.parseInline())
	}
}

// parseTransclusion parses '{' '{' '{' ZID '}' '}' '}'
func (cp *zmkP) parseTransclusion() (ast.BlockNode, bool) {
	if cp.countDelim('{') != 3 {
		return nil, false
	}
	inp := cp.inp
	posA, posE := inp.Pos, 0
loop:
	for {
		switch inp.Ch {
		case input.EOS:
			return nil, false
		case '\n', '\r', ' ', '\t':
			if !hasQueryPrefix(inp.Src[posA:]) {
				return nil, false
			}
		case '\\':
			inp.Next()
			switch inp.Ch {
			case input.EOS, '\n', '\r':
				return nil, false
			}
		case '}':
			posE = inp.Pos
			if posA >= posE {
				return nil, false
			}
			inp.Next()
			if inp.Ch != '}' {
				continue
			}
			inp.Next()
			if inp.Ch != '}' {
				continue
			}
			break loop
		}
		inp.Next()
	}
	inp.Next() // consume last '}'
	a := cp.parseBlockAttributes()
	inp.SkipToEOL()
	refText := string(inp.Src[posA:posE])
	ref := ast.ParseReference(refText)
	return &ast.TranscludeNode{Attrs: a, Ref: ref}, true
}

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
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

-
+

-
+




+
+
+


-



+



+
+

-
+



-
+

-



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"bytes"
	"fmt"
	"strings"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
	"zettelstore.de/z/zettel/meta"
)

// parseInlineSlice parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineSlice() ast.InlineSlice {
func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) {
	inp := cp.inp
	var ins ast.InlineSlice
	for inp.Ch != input.EOS {
		in := cp.parseInline()
		if in == nil {
			return ins
			break
		}
		ins = append(ins, in)
	}
	return ins
}

func (cp *zmkP) parseInline() ast.InlineNode {
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
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







-
+

-
-


-
+

-
+

+
+



















+
-
+






-
-
+
+




-
-
-
-
-







				in, success = cp.parseFootnote()
			case '!':
				in, success = cp.parseMark()
			}
		case '{':
			inp.Next()
			if inp.Ch == '{' {
				in, success = cp.parseImage()
				in, success = cp.parseEmbed()
			}
		case '#':
			return cp.parseTag()
		case '%':
			in, success = cp.parseComment()
		case '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':':
		case '_', '*', '>', '~', '^', ',', '"', '#', ':':
			in, success = cp.parseFormat()
		case '+', '`', '=', runeModGrave:
		case '@', '\'', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '$':
			in, success = cp.parseLiteralMath()
		case '\\':
			return cp.parseBackslash()
		case '-':
			in, success = cp.parseNdash()
		case '&':
			in, success = cp.parseEntity()
		}
		if success {
			return in
		}
	}
	inp.SetPos(pos)
	return cp.parseText()
}

func (cp *zmkP) parseText() *ast.TextNode {
	inp := cp.inp
	pos := inp.Pos
	if inp.Ch == '\\' {
		cp.inp.Next()
		return cp.parseTextBackslash()
		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, '=', '\\', '-', '&':
			return &ast.TextNode{Text: inp.Src[pos:inp.Pos]}
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseTextBackslash() *ast.TextNode {
	cp.inp.Next()
	return cp.parseBackslashRest()
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp
	inp.Next()
	switch inp.Ch {
	case '\n', '\r':
		inp.EatEOL()
		return &ast.BreakNode{Hard: true}
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
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







-
+










-
+










-
-
+
+

-
-
-
-
-
-

-
-
+
+
-







+
+
+
+
-
+



+
+
+
+

+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+





-
+





+
+
+
-
+













+
+
+
+
+
+
+
+
+
+
+
+
+
+













+


+
+
+
+
+
+
+
+
+
-
-
+
+
+







	}
	if inp.Ch == ' ' {
		inp.Next()
		return &ast.TextNode{Text: "\u00a0"}
	}
	pos := inp.Pos
	inp.Next()
	return &ast.TextNode{Text: inp.Src[pos:inp.Pos]}
	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: inp.Src[pos:inp.Pos]}
			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, ins, ok := cp.parseReference(']'); ok {
		attrs := cp.parseAttributes(false)
	if ref, is, ok := cp.parseReference('[', ']'); ok {
		attrs := cp.parseInlineAttributes()
		if len(ref) > 0 {
			onlyRef := false
			r := ast.ParseReference(ref)
			if ins == nil {
				ins = ast.InlineSlice{&ast.TextNode{Text: ref}}
				onlyRef = true
			}
			return &ast.LinkNode{
				Ref:     r,
				Inlines: ins,
				Ref:     ast.ParseReference(ref),
				Inlines: is,
				OnlyRef: onlyRef,
				Attrs:   attrs,
			}, true
		}
	}
	return nil, false
}

func hasQueryPrefix(src []byte) bool {
	return len(src) > len(ast.QueryPrefix) && string(src[:len(ast.QueryPrefix)]) == ast.QueryPrefix
}

func (cp *zmkP) parseReference(closeCh rune) (ref string, ins ast.InlineSlice, ok bool) {
func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, is ast.InlineSlice, _ bool) {
	inp := cp.inp
	inp.Next()
	cp.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)
	if !ok {
		return "", nil, false
	}
	if inp.Ch == '|' { // First part must be inline text
		if pos == inp.Pos { // [[| or {{|
			return "", nil, false
		}
		sepPos := inp.Pos
		hasSpace, ok := cp.readReferenceToSep(closeCh)
		if !ok {
			return "", nil, false
		}
		if inp.Ch == '|' { // First part must be inline text
			if pos == inp.Pos { // [[| or {{|
				return "", nil, false
			}
			cp.inp = input.NewInput(inp.Src[pos:inp.Pos])
		inp.SetPos(pos)
		for inp.Pos < sepPos {
			ins = append(ins, cp.parseInline())
		}
		inp.Next()
		pos = inp.Pos
	} else if hasSpace {
		return "", nil, false
	}

	inp.SetPos(pos)
			for {
				in := cp.parseInline()
				if in == nil {
					break
				}
				is = append(is, in)
			}
			cp.inp = inp
			inp.Next()
		} else {
			if hasSpace {
				return "", nil, false
			}
			inp.SetPos(pos)
		}
	}

	cp.skipSpace()
	pos = inp.Pos
	if !cp.readReferenceToClose(closeCh) {
		return "", nil, false
	}
	ref = inp.Src[pos:inp.Pos]
	ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	inp.Next()
	if inp.Ch != closeCh {
		return "", nil, false
	}
	inp.Next()
	if len(is) == 0 {
		return ref, nil, true
	}
	return ref, ins, true
	return ref, is, true
}

func (cp *zmkP) readReferenceToSep(closeCh rune) (bool, bool) {
	hasSpace := false
	inp := cp.inp
	for {
		switch inp.Ch {
		case input.EOS:
			return false, false
		case '\n', '\r', ' ':
			hasSpace = true
		case '|':
			return hasSpace, true
		case '\\':
			inp.Next()
			switch inp.Ch {
			case input.EOS:
				return false, false
			case '\n', '\r':
				hasSpace = true
			}
		case '%':
			inp.Next()
			if inp.Ch == '%' {
				inp.SkipToEOL()
			}
			continue
		case closeCh:
			inp.Next()
			if inp.Ch == closeCh {
				return hasSpace, true
			}
			continue
		}
		inp.Next()
	}
}

func (cp *zmkP) readReferenceToClose(closeCh rune) bool {
	inp := cp.inp
	pos := inp.Pos
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '\t', '\r', '\n', ' ':
			if !hasQueryPrefix(inp.Src[pos:]) {
				return false
			}
		case '\\':
			inp.Next()
			switch inp.Ch {
		case input.EOS, '\n', '\r', ' ':
			return false
			case input.EOS, '\n', '\r':
				return false
			}
		case closeCh:
			return true
		}
		inp.Next()
	}
}

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
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







-
-
+
+








-
+





-
+












+
+
+



-
-
-
+
+
+


+
+
-
+
+
+









-
+





-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-

-
-
+
-
-
+











+




-
+
+
+
+
+





-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+





-
+








-
+








-
+




-
+







-
+
+


-
+

+





-
+







-

-
+








-
+
-
-

-
+



-
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







	case ' ', ',', '|':
		inp.Next()
	}
	ins, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := cp.parseAttributes(false)
	return &ast.CiteNode{Key: inp.Src[pos:posL], Inlines: ins, Attrs: attrs}, true
	attrs := cp.parseInlineAttributes()
	return &ast.CiteNode{Key: string(inp.Src[pos:posL]), Inlines: ins, Attrs: attrs}, true
}

func (cp *zmkP) parseFootnote() (*ast.FootnoteNode, bool) {
	cp.inp.Next()
	ins, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := cp.parseAttributes(false)
	attrs := cp.parseInlineAttributes()
	return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true
}

func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) {
	cp.skipSpace()
	var ins ast.InlineSlice
	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) {
			return nil, false
		}
	}
	inp.Next()
	if len(ins) == 0 {
		return nil, true
	}
	return ins, true
}

func (cp *zmkP) parseImage() (ast.InlineNode, bool) {
	if ref, ins, ok := cp.parseReference('}'); ok {
		attrs := cp.parseAttributes(false)
func (cp *zmkP) parseEmbed() (ast.InlineNode, bool) {
	if ref, ins, ok := cp.parseReference('{', '}'); ok {
		attrs := cp.parseInlineAttributes()
		if len(ref) > 0 {
			r := ast.ParseReference(ref)
			return &ast.EmbedRefNode{
				Ref:     r,
			return &ast.ImageNode{Ref: r, Inlines: ins, Attrs: attrs}, true
				Inlines: ins,
				Attrs:   attrs,
			}, true
		}
	}
	return nil, false
}

func (cp *zmkP) parseMark() (*ast.MarkNode, bool) {
	inp := cp.inp
	inp.Next()
	pos := inp.Pos
	for inp.Ch != ']' {
	for inp.Ch != '|' && inp.Ch != ']' {
		if !isNameRune(inp.Ch) {
			return nil, false
		}
		inp.Next()
	}
	mn := &ast.MarkNode{Text: inp.Src[pos:inp.Pos]}
	inp.Next()
	return mn, true
}

	mark := inp.Src[pos:inp.Pos]
	ins := ast.InlineSlice{}
	if inp.Ch == '|' {
		inp.Next()
		var ok bool
		ins, ok = cp.parseLinkLikeRest()
		if !ok {
			return nil, false
		}
	} else {
func (cp *zmkP) parseTag() ast.InlineNode {
	inp := cp.inp
	posH := inp.Pos
	inp.Next()
		inp.Next()
	pos := inp.Pos
	for isNameRune(inp.Ch) {
		inp.Next()
	}
	if pos == inp.Pos || inp.Ch == '#' {
		return &ast.TextNode{Text: inp.Src[posH:inp.Pos]}
	mn := &ast.MarkNode{Mark: string(mark), Inlines: ins}
	}
	return &ast.TagNode{Tag: inp.Src[pos:inp.Pos]}
	return mn, true
}

func (cp *zmkP) parseComment() (res *ast.LiteralNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	attrs := cp.parseInlineAttributes()
	cp.skipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return &ast.LiteralNode{Code: ast.LiteralComment, Text: inp.Src[pos:inp.Pos]}, true
			return &ast.LiteralNode{
				Kind:    ast.LiteralComment,
				Attrs:   attrs,
				Content: append([]byte(nil), inp.Src[pos:inp.Pos]...),
			}, true
		}
		inp.Next()
	}
}

var mapRuneFormat = map[rune]ast.FormatCode{
var mapRuneFormat = map[rune]ast.FormatKind{
	'/':  ast.FormatItalic,
	'*':  ast.FormatBold,
	'_':  ast.FormatUnder,
	'~':  ast.FormatStrike,
	'\'': ast.FormatMonospace,
	'^':  ast.FormatSuper,
	',':  ast.FormatSub,
	'_': ast.FormatEmph,
	'*': ast.FormatStrong,
	'>': ast.FormatInsert,
	'~': ast.FormatDelete,
	'^': ast.FormatSuper,
	',': ast.FormatSub,
	'<':  ast.FormatQuotation,
	'"':  ast.FormatQuote,
	';':  ast.FormatSmall,
	':':  ast.FormatSpan,
	'"': ast.FormatQuote,
	'#': ast.FormatMark,
	':': ast.FormatSpan,
}

func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	code, ok := mapRuneFormat[fch]
	kind, ok := mapRuneFormat[fch]
	if !ok {
		panic(fmt.Sprintf("%q is not a formatting char", fch))
	}
	inp.Next() // read 2nd formatting character
	if inp.Ch != fch {
		return nil, false
	}
	inp.Next()
	fn := &ast.FormatNode{Code: code}
	fn := &ast.FormatNode{Kind: kind, Inlines: nil}
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			inp.Next()
			if inp.Ch == fch {
				inp.Next()
				fn.Attrs = cp.parseAttributes(false)
				fn.Attrs = cp.parseInlineAttributes()
				return fn, true
			}
			fn.Inlines = append(fn.Inlines, &ast.TextNode{Text: string(fch)})
		} else if in := cp.parseInline(); in != nil {
			if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
			if _, ok = in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
				return nil, false
			}
			fn.Inlines = append(fn.Inlines, in)
		}
	}
}

var mapRuneLiteral = map[rune]ast.LiteralCode{
var mapRuneLiteral = map[rune]ast.LiteralKind{
	'@':          ast.LiteralZettel,
	'`':          ast.LiteralProg,
	runeModGrave: ast.LiteralProg,
	'+':          ast.LiteralKeyb,
	'\'':         ast.LiteralInput,
	'=':          ast.LiteralOutput,
	// No '$': ast.LiteralMath, because paring literal math is a little different
}

func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	code, ok := mapRuneLiteral[fch]
	kind, ok := mapRuneLiteral[fch]
	if !ok {
		panic(fmt.Sprintf("%q is not a formatting char", fch))
	}
	inp.Next() // read 2nd formatting character
	if inp.Ch != fch {
		return nil, false
	}
	fn := &ast.LiteralNode{Code: code}
	inp.Next()
	var sb strings.Builder
	var buf bytes.Buffer
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Peek() == fch {
				inp.Next()
				inp.Next()
				fn.Attrs = cp.parseAttributes(false)
				return createLiteralNode(kind, cp.parseInlineAttributes(), buf.Bytes()), true
				fn.Text = sb.String()
				return fn, true
			}
			sb.WriteRune(fch)
			buf.WriteRune(fch)
			inp.Next()
		} else {
			tn := cp.parseText()
			sb.WriteString(tn.Text)
			buf.WriteString(tn.Text)
		}
	}
}

func createLiteralNode(kind ast.LiteralKind, a attrs.Attributes, content []byte) *ast.LiteralNode {
	if kind == ast.LiteralZettel {
		if val, found := a.Get(""); found && val == meta.SyntaxHTML {
			kind = ast.LiteralHTML
			a = a.Remove("")
		}
	}
	return &ast.LiteralNode{
		Kind:    kind,
		Attrs:   a,
		Content: content,
	}
}

func (cp *zmkP) parseLiteralMath() (res ast.InlineNode, success bool) {
	inp := cp.inp
	inp.Next() // read 2nd formatting character
	if inp.Ch != '$' {
		return nil, false
	}
	inp.Next()
	pos := inp.Pos
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == '$' && inp.Peek() == '$' {
			content := append([]byte{}, inp.Src[pos:inp.Pos]...)
			inp.Next()
			inp.Next()
			fn := &ast.LiteralNode{
				Kind:    ast.LiteralMath,
				Attrs:   cp.parseInlineAttributes(),
				Content: content,
			}
			return fn, true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) {
	inp := cp.inp
	if inp.Peek() != inp.Ch {
		return nil, false

Changes to parser/zettelmark/node.go.

1
2

3
4

5
6
7
8



9
10
11
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




-
+

-
+




+
+
+


-


-
-
+
-









-
-
-




-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"zettelstore.de/z/ast"
import "zettelstore.de/z/ast"
)

// Internal nodes for parsing zettelmark. These will be removed in
// post-processing.

// nullItemNode specifies a removable placeholder for an item node.
type nullItemNode struct {
	ast.ItemNode
}

// Accept a visitor and visit the node.
func (nn *nullItemNode) Accept(v ast.Visitor) {}

// nullDescriptionNode specifies a removable placeholder.
type nullDescriptionNode struct {
	ast.DescriptionNode
}

// Accept a visitor and visit the node.
func (nn *nullDescriptionNode) Accept(v ast.Visitor) {}

Changes to parser/zettelmark/post-processor.go.

1
2

3
4

5
6
7
8



9
10
11
12
13
14
15
16
17
18
19
20
21

22
23

24
25
26
27

28
29

30
31
32
33
34
35
36
37
38
39
40






























41

42
43
44
45
46
47
48

49
50

51
52
53




54
55
56
57
58
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22

23
24

25
26
27
28

29
30

31
32
33
34
35
36
37
38




39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

-
+

-
+




+
+
+


-









-
+

-
+



-
+

-
+







-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+


-
-
-
-
-
+

-
+


-
+
+
+
+

-


-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
-
+
+






-
-
+


-
+
















-
+

-
-
+
+


-
+


-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"strings"

	"zettelstore.de/z/ast"
)

// postProcessBlocks is the entry point for post-processing a list of block nodes.
func postProcessBlocks(bs ast.BlockSlice) ast.BlockSlice {
func postProcessBlocks(bs *ast.BlockSlice) {
	pp := postProcessor{}
	return pp.processBlockSlice(bs)
	ast.Walk(&pp, bs)
}

// postProcessInlines is the entry point for post-processing a list of inline nodes.
func postProcessInlines(is ast.InlineSlice) ast.InlineSlice {
func postProcessInlines(is *ast.InlineSlice) {
	pp := postProcessor{}
	return pp.processInlineSlice(is)
	ast.Walk(&pp, is)
}

// postProcessor is a visitor that cleans the abstract syntax tree.
type postProcessor struct {
	inVerse bool
}

// VisitPara post-processes a paragraph.
func (pp *postProcessor) VisitPara(pn *ast.ParaNode) {
	if pn != nil {
		pn.Inlines = pp.processInlineSlice(pn.Inlines)
func (pp *postProcessor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		pp.visitBlockSlice(n)
	case *ast.InlineSlice:
		pp.visitInlineSlice(n)
	case *ast.ParaNode:
		return pp
	case *ast.RegionNode:
		pp.visitRegion(n)
	case *ast.HeadingNode:
		return pp
	case *ast.NestedListNode:
		pp.visitNestedList(n)
	case *ast.DescriptionListNode:
		pp.visitDescriptionList(n)
	case *ast.TableNode:
		pp.visitTable(n)
	case *ast.LinkNode:
		return pp
	case *ast.EmbedRefNode:
		return pp
	case *ast.EmbedBLOBNode:
		return pp
	case *ast.CiteNode:
		return pp
	case *ast.FootnoteNode:
		return pp
	case *ast.FormatNode:
		return pp
	}
	return nil
}

// VisitVerbatim does nothing, no post-processing needed.
func (pp *postProcessor) VisitVerbatim(vn *ast.VerbatimNode) {}

// VisitRegion post-processes a region.
func (pp *postProcessor) VisitRegion(rn *ast.RegionNode) {
func (pp *postProcessor) visitRegion(rn *ast.RegionNode) {
	oldVerse := pp.inVerse
	if rn.Code == ast.RegionVerse {
	if rn.Kind == ast.RegionVerse {
		pp.inVerse = true
	}
	rn.Blocks = pp.processBlockSlice(rn.Blocks)
	pp.visitBlockSlice(&rn.Blocks)
	if len(rn.Inlines) > 0 {
		pp.visitInlineSlice(&rn.Inlines)
	}
	pp.inVerse = oldVerse
	rn.Inlines = pp.processInlineSlice(rn.Inlines)
}

// VisitHeading post-processes a heading.
func (pp *postProcessor) VisitHeading(hn *ast.HeadingNode) {
	hn.Inlines = pp.processInlineSlice(hn.Inlines)
}

// VisitHRule does nothing, no post-processing needed.
func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}
	if ln.Kind != ast.NestedListQuote {
		return
	}
	items := []ast.ItemSlice{}
	collectedInlines := ast.InlineSlice{}

func (pp *postProcessor) VisitHRule(hn *ast.HRuleNode) {}

// VisitList post-processes a list.
	addCollectedParagraph := func() {
		if len(collectedInlines) > 1 {
			items = append(items, []ast.ItemNode{&ast.ParaNode{Inlines: collectedInlines[1:]}})
			collectedInlines = ast.InlineSlice{}
		}
	}

func (pp *postProcessor) VisitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}
}

// VisitDescriptionList post-processes a description list.
func (pp *postProcessor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	for _, item := range ln.Items {
		if len(item) != 1 { // i.e. 0 or > 1
			addCollectedParagraph()
			items = append(items, item)
			continue
		}

		// len(item) == 1
		if pn, ok := item[0].(*ast.ParaNode); ok {
			collectedInlines = append(collectedInlines, &ast.BreakNode{})
			collectedInlines = append(collectedInlines, pn.Inlines...)
			continue
		}

		addCollectedParagraph()
		items = append(items, item)
	}
	addCollectedParagraph()
	ln.Items = items
}

func (pp *postProcessor) visitDescriptionList(dn *ast.DescriptionListNode) {
	for i, def := range dn.Descriptions {
		if len(def.Term) > 0 {
		dn.Descriptions[i].Term = pp.processInlineSlice(def.Term)
			ast.Walk(pp, &dn.Descriptions[i].Term)
		}
		for j, b := range def.Descriptions {
			dn.Descriptions[i].Descriptions[j] = pp.processDescriptionSlice(b)
		}
	}
}

// VisitTable post-processes a table.
func (pp *postProcessor) VisitTable(tn *ast.TableNode) {
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)
	}
	if len(tn.Header) > 0 {
		tn.Header = appendCells(tn.Header, width, tn.Align)
		for i, cell := range tn.Header {
			pp.processCell(cell, tn.Align[i])
		}
	}
	pp.visitTableRows(tn, width)
}

func (pp *postProcessor) visitTableHeader(tn *ast.TableNode) {
func (*postProcessor) visitTableHeader(tn *ast.TableNode) {
	for pos, cell := range tn.Header {
		inlines := cell.Inlines
		if len(inlines) == 0 {
		ins := cell.Inlines
		if len(ins) == 0 {
			continue
		}
		if textNode, ok := inlines[0].(*ast.TextNode); ok {
		if textNode, ok := ins[0].(*ast.TextNode); ok {
			textNode.Text = strings.TrimPrefix(textNode.Text, "=")
		}
		if textNode, ok := inlines[len(inlines)-1].(*ast.TextNode); ok {
		if textNode, ok := ins[len(ins)-1].(*ast.TextNode); ok {
			if tnl := len(textNode.Text); tnl > 0 {
				if align := getAlignment(textNode.Text[tnl-1]); align != ast.AlignDefault {
					tn.Align[pos] = align
					textNode.Text = textNode.Text[0 : tnl-1]
				}
			}
		}
140
141
142
143
144
145
146
147




148
149
150
151
152
153
154
155



156
157
158
159
160
161
162
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







-
+
+
+
+





-
-
-
+
+
+







		}
	}
	return width
}

func appendCells(row ast.TableRow, width int, colAlign []ast.Alignment) ast.TableRow {
	for len(row) < width {
		row = append(row, &ast.TableCell{Align: colAlign[len(row)]})
		row = append(row, &ast.TableCell{
			Align:   colAlign[len(row)],
			Inlines: nil,
		})
	}
	return row
}

func isHeaderRow(row ast.TableRow) bool {
	for i := 0; i < len(row); i++ {
		if inlines := row[i].Inlines; len(inlines) > 0 {
			if textNode, ok := inlines[0].(*ast.TextNode); ok {
	for _, cell := range row {
		if is := cell.Inlines; len(is) > 0 {
			if textNode, ok := is[0].(*ast.TextNode); ok {
				if strings.HasPrefix(textNode.Text, "=") {
					return true
				}
			}
		}
	}
	return false
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
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







-
+
-
-
-
-
+



-
+





-
+


-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
-
-
+
-
-
-
-

-
-
+
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
+
-
-
-
-
-
+
+


-
-
+
+

-
+










-
-
+
+

-
+





+
+
+

-
+







	default:
		return ast.AlignDefault
	}
}

// processCell tries to recognize cell formatting.
func (pp *postProcessor) processCell(cell *ast.TableCell, colAlign ast.Alignment) {
	if len(cell.Inlines) == 0 {
	if tn := initialText(cell.Inlines); tn != nil {
		return
	}
	if textNode, ok := cell.Inlines[0].(*ast.TextNode); ok && len(textNode.Text) > 0 {
		align := getAlignment(textNode.Text[0])
		align := getAlignment(tn.Text[0])
		if align == ast.AlignDefault {
			cell.Align = colAlign
		} else {
			textNode.Text = textNode.Text[1:]
			tn.Text = tn.Text[1:]
			cell.Align = align
		}
	} else {
		cell.Align = colAlign
	}
	cell.Inlines = pp.processInlineSlice(cell.Inlines)
	ast.Walk(pp, &cell.Inlines)
}

// VisitBLOB does nothing.
func (pp *postProcessor) VisitBLOB(bn *ast.BLOBNode) {}

func initialText(ins ast.InlineSlice) *ast.TextNode {
// VisitText does nothing.
func (pp *postProcessor) VisitText(tn *ast.TextNode) {}

	if len(ins) == 0 {
// VisitTag does nothing.
func (pp *postProcessor) VisitTag(tn *ast.TagNode) {}

		return nil
// VisitSpace does nothing.
func (pp *postProcessor) VisitSpace(sn *ast.SpaceNode) {}

	}
// VisitBreak does nothing.
func (pp *postProcessor) VisitBreak(bn *ast.BreakNode) {}

	if tn, ok := ins[0].(*ast.TextNode); ok && len(tn.Text) > 0 {
// VisitLink post-processes a link.
func (pp *postProcessor) VisitLink(ln *ast.LinkNode) {
	ln.Inlines = pp.processInlineSlice(ln.Inlines)
}

		return tn
// VisitImage post-processes an image.
func (pp *postProcessor) VisitImage(in *ast.ImageNode) {
	if len(in.Inlines) > 0 {
		in.Inlines = pp.processInlineSlice(in.Inlines)
	}
}

	return nil
// VisitCite post-processes a citation.
func (pp *postProcessor) VisitCite(cn *ast.CiteNode) {
	cn.Inlines = pp.processInlineSlice(cn.Inlines)
}

}
// VisitFootnote post-processes a footnote.
func (pp *postProcessor) VisitFootnote(fn *ast.FootnoteNode) {
	fn.Inlines = pp.processInlineSlice(fn.Inlines)
}


// VisitMark post-processes a mark.
func (pp *postProcessor) VisitMark(mn *ast.MarkNode) {}

func (pp *postProcessor) visitBlockSlice(bs *ast.BlockSlice) {
	if bs == nil {
var mapSemantic = map[ast.FormatCode]ast.FormatCode{
	ast.FormatItalic: ast.FormatEmph,
	ast.FormatBold:   ast.FormatStrong,
	ast.FormatUnder:  ast.FormatInsert,
	ast.FormatStrike: ast.FormatDelete,
}

		return
	}
	if len(*bs) == 0 {
// VisitFormat post-processes formatted inline nodes.
func (pp *postProcessor) VisitFormat(fn *ast.FormatNode) {
	if fn.Attrs != nil && fn.Attrs.HasDefault() {
		if newCode, ok := mapSemantic[fn.Code]; ok {
			fn.Attrs.RemoveDefault()
			fn.Code = newCode
		*bs = nil
		}
	}
	fn.Inlines = pp.processInlineSlice(fn.Inlines)
}

		return
// VisitLiteral post-processes an inline literal.
func (pp *postProcessor) VisitLiteral(cn *ast.LiteralNode) {}

	}
// processBlockSlice post-processes a slice of blocks.
// It is one of the working horses for post-processing.
func (pp *postProcessor) processBlockSlice(bns ast.BlockSlice) ast.BlockSlice {
	for _, bn := range bns {
		bn.Accept(pp)
	for _, bn := range *bs {
		ast.Walk(pp, bn)
	}
	fromPos, toPos := 0, 0
	for fromPos < len(bns) {
		bns[toPos] = bns[fromPos]
	for fromPos < len(*bs) {
		(*bs)[toPos] = (*bs)[fromPos]
		fromPos++
		switch bn := bns[toPos].(type) {
		switch bn := (*bs)[toPos].(type) {
		case *ast.ParaNode:
			if len(bn.Inlines) > 0 {
				toPos++
			}
		case *nullItemNode:
		case *nullDescriptionNode:
		default:
			toPos++
		}
	}
	for pos := toPos; pos < len(bns); pos++ {
		bns[pos] = nil // Allow excess nodes to be garbage collected.
	for pos := toPos; pos < len(*bs); pos++ {
		(*bs)[pos] = nil // Allow excess nodes to be garbage collected.
	}
	return bns[:toPos:toPos]
	*bs = (*bs)[:toPos:toPos]
}

// processItemSlice post-processes a slice of items.
// It is one of the working horses for post-processing.
func (pp *postProcessor) processItemSlice(ins ast.ItemSlice) ast.ItemSlice {
	if len(ins) == 0 {
		return nil
	}
	for _, in := range ins {
		in.Accept(pp)
		ast.Walk(pp, in)
	}
	fromPos, toPos := 0, 0
	for fromPos < len(ins) {
		ins[toPos] = ins[fromPos]
		fromPos++
		switch in := ins[toPos].(type) {
		case *ast.ParaNode:
308
309
310
311
312
313
314



315
316

317
318
319
320
321
322
323
315
316
317
318
319
320
321
322
323
324
325

326
327
328
329
330
331
332
333







+
+
+

-
+







	}
	return ins[:toPos:toPos]
}

// processDescriptionSlice post-processes a slice of descriptions.
// It is one of the working horses for post-processing.
func (pp *postProcessor) processDescriptionSlice(dns ast.DescriptionSlice) ast.DescriptionSlice {
	if len(dns) == 0 {
		return nil
	}
	for _, dn := range dns {
		dn.Accept(pp)
		ast.Walk(pp, dn)
	}
	fromPos, toPos := 0, 0
	for fromPos < len(dns) {
		dns[toPos] = dns[fromPos]
		fromPos++
		switch dn := dns[toPos].(type) {
		case *ast.ParaNode:
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
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



















-
-
-
-
-
+
+
+
+
+
+
+

-
-
+
+


-
-
+
-
-
-
-
+
+
+
-
-



-
-
-
+
+
+
+

+
+
+
+


+
-
+


+
-
+


-
+








-
+
+

-
-
-
-
-
+
+
+
+
-
-
-
+
+
-
-
+
-
-
-
+
-
-
+
-






-
-
-
-
+
-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
+
-
-
+







-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+
+
















-
-
-
-
-
-
-
-
-
-
-
-
	}
	for pos := toPos; pos < len(dns); pos++ {
		dns[pos] = nil // Allow excess nodes to be garbage collected.
	}
	return dns[:toPos:toPos]
}

// processInlineSlice post-processes a slice of inline nodes.
// It is one of the working horses for post-processing.
func (pp *postProcessor) processInlineSlice(ins ast.InlineSlice) ast.InlineSlice {
	if len(ins) == 0 {
		return nil
func (pp *postProcessor) visitInlineSlice(is *ast.InlineSlice) {
	if is == nil {
		return
	}
	if len(*is) == 0 {
		*is = nil
		return
	}
	for _, in := range ins {
		in.Accept(pp)
	for _, in := range *is {
		ast.Walk(pp, in)
	}

	if !pp.inVerse {
		ins = processInlineSliceHead(ins)
	pp.processInlineSliceHead(is)
	}
	toPos := pp.processInlineSliceCopy(ins)
	toPos = pp.processInlineSliceTail(ins, toPos)
	ins = ins[:toPos:toPos]
	toPos := pp.processInlineSliceCopy(is)
	toPos = pp.processInlineSliceTail(is, toPos)
	*is = (*is)[:toPos:toPos]
	pp.processInlineSliceInplace(ins)
	return ins
}

// processInlineSliceHead removes leading spaces and empty text.
func processInlineSliceHead(ins ast.InlineSlice) ast.InlineSlice {
	for i := 0; i < len(ins); i++ {
		switch in := ins[i].(type) {
func (pp *postProcessor) processInlineSliceHead(is *ast.InlineSlice) {
	ins := *is
	for i, in := range ins {
		switch in := in.(type) {
		case *ast.SpaceNode:
			if pp.inVerse {
				*is = ins[i:]
				return
			}
		case *ast.TextNode:
			if len(in.Text) > 0 {
				*is = ins[i:]
				return ins[i:]
				return
			}
		default:
			*is = ins[i:]
			return ins[i:]
			return
		}
	}
	return ins[0:0]
	*is = ins[0:0]
}

// processInlineSliceCopy goes forward through the slice and tries to eliminate
// elements that follow the current element.
//
// Two text nodes are merged into one.
//
// Two spaces following a break are merged into a hard break.
func (pp *postProcessor) processInlineSliceCopy(ins ast.InlineSlice) int {
func (pp *postProcessor) processInlineSliceCopy(is *ast.InlineSlice) int {
	ins := *is
	maxPos := len(ins)
	for {
		again, toPos := pp.processInlineSliceCopyLoop(ins, maxPos)
		for pos := toPos; pos < maxPos; pos++ {
			ins[pos] = nil // Allow excess nodes to be garbage collected.
		}
	toPos := pp.processInlineSliceCopyLoop(is, maxPos)
	for pos := toPos; pos < maxPos; pos++ {
		ins[pos] = nil // Allow excess nodes to be garbage collected.
	}
		if !again {
			return toPos
		}
	return toPos
}
		maxPos = toPos
	}

}

func (pp *postProcessor) processInlineSliceCopyLoop(
func (pp *postProcessor) processInlineSliceCopyLoop(is *ast.InlineSlice, maxPos int) int {
	ins ast.InlineSlice, maxPos int) (bool, int) {

	ins := *is
	again := false
	fromPos, toPos := 0, 0
	for fromPos < maxPos {
		ins[toPos] = ins[fromPos]
		fromPos++
		switch in := ins[toPos].(type) {
		case *ast.TextNode:
			for fromPos < maxPos {
				if tn, ok := ins[fromPos].(*ast.TextNode); ok {
					in.Text = in.Text + tn.Text
					fromPos++
			fromPos = processTextNode(ins, maxPos, in, fromPos)
				} else {
					break
				}
			}
		case *ast.SpaceNode:
			if fromPos < maxPos {
				switch nn := ins[fromPos].(type) {
				case *ast.BreakNode:
					if len(in.Lexeme) > 1 {
						nn.Hard = true
						ins[toPos] = nn
						fromPos++
					}
				case *ast.TextNode:
					if pp.inVerse {
						ins[toPos] = &ast.TextNode{Text: strings.Repeat("\u00a0", len(in.Lexeme)) + nn.Text}
			if pp.inVerse {
				in.Lexeme = strings.Repeat("\u00a0", in.Count())
						fromPos++
						again = true
					}
			}
				}
			}
			fromPos = processSpaceNode(ins, maxPos, in, toPos, fromPos)
		case *ast.BreakNode:
			if pp.inVerse {
				in.Hard = true
			}
		}
		toPos++
	}
	return again, 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 (pp *postProcessor) processInlineSliceTail(ins ast.InlineSlice, toPos int) int {
func (*postProcessor) processInlineSliceTail(is *ast.InlineSlice, toPos int) int {
	ins := *is
	for toPos > 0 {
		switch n := ins[toPos-1].(type) {
		case *ast.TextNode:
			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 (pp *postProcessor) processInlineSliceInplace(ins ast.InlineSlice) {
	for _, in := range ins {
		if n, ok := in.(*ast.TextNode); ok {
			if n.Text == "..." {
				n.Text = "\u2026"
			} else if len(n.Text) == 4 && strings.IndexByte(",;:!?", n.Text[3]) >= 0 && n.Text[:3] == "..." {
				n.Text = "\u2026" + n.Text[3:]
			}
		}
	}
}

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
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
54
55
56
57
58

-
+

-
+




+
+
+






+


-
-
-
-
+
+
+
+
+




-
-
-
-
+
+
+
+
+
+
+



-
+


-
+
+


-
+


-
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"strings"
	"unicode"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:         meta.ValueSyntaxZmk,
		AltNames:     nil,
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
		Name:          meta.SyntaxZmk,
		AltNames:      nil,
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice {
	parser := &zmkP{inp: inp}
	bs := parser.parseBlockSlice()
	return postProcessBlocks(bs)
	postProcessBlocks(&bs)
	return bs
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
	parser := &zmkP{inp: inp}
	is := parser.parseInlineSlice()
	return postProcessInlines(is)
	postProcessInlines(&is)
	return is
}

type zmkP struct {
	inp          *input.Input             // Input stream
	lists        []*ast.NestedListNode    // Stack of lists
	table        *ast.TableNode           // Current table
	descrl       *ast.DescriptionListNode // Current description list
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
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







-
+













-
-
-
-
+


-
+
-



-
+






-
+
-
-
-
-
-
-
+






-
+


-
+





-
+


-
-
-
-
-
-








-
+














-
-
-
-
-
-
-
+

-
-
-
-
-
-
-
-
+
+
+
+
+
+
+

-
-
-
+
+
+
+

+
+

-
+
-






+
-
+





-
-
+
+



-
+


-
+


-
+














-
+

-
-
+
+



-
+








-
-
-








-
+
-
-
-
-







// clearStacked removes all multi-line nodes from parser.
func (cp *zmkP) clearStacked() {
	cp.lists = nil
	cp.table = nil
	cp.descrl = nil
}

func (cp *zmkP) parseNormalAttribute(attrs map[string]string, sameLine bool) bool {
func (cp *zmkP) parseNormalAttribute(attrs map[string]string) bool {
	inp := cp.inp
	posK := inp.Pos
	for isNameRune(inp.Ch) {
		inp.Next()
	}
	if posK == inp.Pos {
		return false
	}
	key := string(inp.Src[posK:inp.Pos])
	if inp.Ch != '=' {
		attrs[key] = ""
		return true
	}
	if sameLine && input.IsEOLEOS(inp.Ch) {
		return false
	}
	return cp.parseAttributeValue(key, attrs, sameLine)
	return cp.parseAttributeValue(key, attrs)
}

func (cp *zmkP) parseAttributeValue(
func (cp *zmkP) parseAttributeValue(key string, attrs map[string]string) bool {
	key string, attrs map[string]string, sameLine bool) bool {
	inp := cp.inp
	inp.Next()
	if inp.Ch == '"' {
		return cp.parseQuotedAttributeValue(key, attrs, sameLine)
		return cp.parseQuotedAttributeValue(key, attrs)
	}
	posV := inp.Pos
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '\n', '\r':
		case '\n', '\r', ' ', '}':
			if sameLine {
				return false
			}
			fallthrough
		case ' ', '}':
			updateAttrs(attrs, key, inp.Src[posV:inp.Pos])
			updateAttrs(attrs, key, string(inp.Src[posV:inp.Pos]))
			return true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string, sameLine bool) bool {
func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string) bool {
	inp := cp.inp
	inp.Next()
	var val string
	var sb strings.Builder
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '"':
			updateAttrs(attrs, key, val)
			updateAttrs(attrs, key, sb.String())
			inp.Next()
			return true
		case '\n', '\r':
			if sameLine {
				return false
			}
			inp.EatEOL()
			val += " "
		case '\\':
			inp.Next()
			switch inp.Ch {
			case input.EOS, '\n', '\r':
				return false
			}
			fallthrough
		default:
			val += string(inp.Ch)
			sb.WriteRune(inp.Ch)
			inp.Next()
		}
	}

}

func updateAttrs(attrs map[string]string, key, val string) {
	if prevVal := attrs[key]; len(prevVal) > 0 {
		attrs[key] = prevVal + " " + val
	} else {
		attrs[key] = val
	}
}

// parseAttributes reads optional attributes.
// If sameLine is True, it is called from block nodes. In this case, a single
// name is allowed. It will parse as {name}. Attributes are not allowed to be
// continued on next line.
// If sameLine is False, it is called from inline nodes. In this case, the next
// rune must be '{'. A continuation on next lines is allowed.
func (cp *zmkP) parseAttributes(sameLine bool) *ast.Attributes {
func (cp *zmkP) parseBlockAttributes() attrs.Attributes {
	inp := cp.inp
	if sameLine {
		pos := inp.Pos
		for isNameRune(inp.Ch) {
			inp.Next()
		}
		if pos < inp.Pos {
			return &ast.Attributes{Attrs: map[string]string{"": inp.Src[pos:inp.Pos]}}
		}
	pos := inp.Pos
	for isNameRune(inp.Ch) {
		inp.Next()
	}
	if pos < inp.Pos {
		return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])}
	}

		// No immediate name: skip spaces
		cp.skipSpace()
	}
	// No immediate name: skip spaces
	cp.skipSpace()
	return cp.parseInlineAttributes()
}

func (cp *zmkP) parseInlineAttributes() attrs.Attributes {
	inp := cp.inp
	pos := inp.Pos
	attrs, success := cp.doParseAttributes(sameLine)
	if attrs, success := cp.doParseAttributes(); success {
	if sameLine || success {
		return attrs
	}
	inp.SetPos(pos)
	return nil
}

// doParseAttributes reads attributes.
func (cp *zmkP) doParseAttributes(sameLine bool) (res *ast.Attributes, success bool) {
func (cp *zmkP) doParseAttributes() (res attrs.Attributes, success bool) {
	inp := cp.inp
	if inp.Ch != '{' {
		return nil, false
	}
	inp.Next()
	attrs := map[string]string{}
	if !cp.parseAttributeValues(sameLine, attrs) {
	a := attrs.Attributes{}
	if !cp.parseAttributeValues(a) {
		return nil, false
	}
	inp.Next()
	return &ast.Attributes{Attrs: attrs}, true
	return a, true
}

func (cp *zmkP) parseAttributeValues(sameLine bool, attrs map[string]string) bool {
func (cp *zmkP) parseAttributeValues(a attrs.Attributes) bool {
	inp := cp.inp
	for {
		cp.skipSpaceLine(sameLine)
		cp.skipSpaceLine()
		switch inp.Ch {
		case input.EOS:
			return false
		case '}':
			return true
		case '.':
			inp.Next()
			posC := inp.Pos
			for isNameRune(inp.Ch) {
				inp.Next()
			}
			if posC == inp.Pos {
				return false
			}
			updateAttrs(attrs, "class", inp.Src[posC:inp.Pos])
			updateAttrs(a, "class", string(inp.Src[posC:inp.Pos]))
		case '=':
			delete(attrs, "")
			if !cp.parseAttributeValue("", attrs, sameLine) {
			delete(a, "")
			if !cp.parseAttributeValue("", a) {
				return false
			}
		default:
			if !cp.parseNormalAttribute(attrs, sameLine) {
			if !cp.parseNormalAttribute(a) {
				return false
			}
		}

		switch inp.Ch {
		case '}':
			return true
		case '\n', '\r':
			if sameLine {
				return false
			}
		case ' ', ',':
			inp.Next()
		default:
			return false
		}
	}
}

func (cp *zmkP) skipSpaceLine(sameLine bool) {
func (cp *zmkP) skipSpaceLine() {
	if sameLine {
		cp.skipSpace()
		return
	}
	for inp := cp.inp; ; {
		switch inp.Ch {
		case ' ':
			inp.Next()
		case '\n', '\r':
			inp.EatEOL()
		default:

Added 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
27
28
29
30
31
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/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
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

-
+

-
+




+
+
+







-



-
-
-
-
-
+
+
+
+
+
-
-
-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zettelmark_test provides some tests for the zettelmarkup parser.
package zettelmark_test

import (
	"fmt"
	"sort"
	"strings"
	"testing"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	// Ensure that the text encoder is available.
	// Needed by parser/cleanup.go
	_ "zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/zettel/meta"
)

type TestCase struct{ source, want string }
type TestCases []TestCase

func replace(s string, tcs TestCases) TestCases {
	var testCases TestCases
43
44
45
46
47
48
49
50
51


52
53

54
55
56
57
58
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
43
44
45
46
47
48
49


50
51
52

53
54
55
56
57
58
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







-
-
+
+

-
+









+










+

















-
-
-
-
-
-
-
-
+




+








+









+









+










-
+


-
-
+
+










-
-
-
+
+
+

-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




+



















+












-
+
+












-
-
-
+
+
+



-
-
+
+

-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+




+



-
+
+

-
+
+

-
-
-
-
+
+
+
+
+
+
+




+












-
+







+
+
-
+





+
+
+
+
















-
-
-
+
+
+




+
-
+



















+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




+

-
-
-
-
+
+
+
+





+







+




-
-
-
+
+
+

+
+
+






+
-
+
+
+
+
+
+
+
+
+
+



-
+
+










+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+












+












+













+






-
-
-
-
-
-
-
+
+
+
+
+
+
+

-
+



-
-
+
+
+
+
+
+
+
+
+
+




+














+








func checkTcs(t *testing.T, tcs TestCases) {
	t.Helper()

	for tcn, tc := range tcs {
		t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) {
			st.Helper()
			inp := input.NewInput(tc.source)
			bns := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk)
			inp := input.NewInput([]byte(tc.source))
			bns := parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML)
			var tv TestVisitor
			tv.visitBlockSlice(bns)
			ast.Walk(&tv, &bns)
			got := tv.String()
			if tc.want != got {
				st.Errorf("\nwant=%q\n got=%q", tc.want, got)
			}
		})
	}
}

func TestEOL(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"", ""},
		{"\n", ""},
		{"\r", ""},
		{"\r\n", ""},
		{"\n\n", ""},
	})
}

func TestText(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"abcd", "(PARA abcd)"},
		{"ab cd", "(PARA ab SP 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)"},
		{"...", "(PARA \u2026)"},
		{"...,", "(PARA \u2026,)"},
		{"...;", "(PARA \u2026;)"},
		{"...:", "(PARA \u2026:)"},
		{"...!", "(PARA \u2026!)"},
		{"...?", "(PARA \u2026?)"},
		{"...-", "(PARA ...-)"},
		{"a...b", "(PARA a...b)"},
		{"http://a, http://b", "(PARA http://a, SP http://b)"},
	})
}

func TestSpace(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{" ", ""},
		{"\t", ""},
		{"  ", ""},
	})
}

func TestSoftBreak(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"x\ny", "(PARA x SB y)"},
		{"z\n", "(PARA z)"},
		{" \n ", ""},
		{" \n", ""},
	})
}

func TestHardBreak(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"x  \ny", "(PARA x HB y)"},
		{"z  \n", "(PARA z)"},
		{"   \n ", ""},
		{"   \n", ""},
	})
}

func TestLink(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[", "(PARA [)"},
		{"[[", "(PARA [[)"},
		{"[[|", "(PARA [[|)"},
		{"[[]", "(PARA [[])"},
		{"[[|]", "(PARA [[|])"},
		{"[[]]", "(PARA [[]])"},
		{"[[|]]", "(PARA [[|]])"},
		{"[[ ]]", "(PARA [[ SP ]])"},
		{"[[\n]]", "(PARA [[ SB ]])"},
		{"[[ a]]", "(PARA (LINK a a))"},
		{"[[ a]]", "(PARA (LINK a))"},
		{"[[a ]]", "(PARA [[a SP ]])"},
		{"[[a\n]]", "(PARA [[a SB ]])"},
		{"[[a]]", "(PARA (LINK a a))"},
		{"[[12345678901234]]", "(PARA (LINK 12345678901234 12345678901234))"},
		{"[[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))"},
		{"[[a]]go", "(PARA (LINK a a) go)"},
		{"[[a]]{go}", "(PARA (LINK a a)[ATTR go])"},
		{"[[[[a]]|b]]", "(PARA (LINK [[a [[a) |b]])"},
		{"[[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))"},
		{"[[[b]c|d]]", "(PARA [ (LINK d b]c))"},
		{"[[a[]c|d]]", "(PARA (LINK d a[]c))"},
		{"[[a[b]|d]]", "(PARA (LINK d a[b]))"},
		{"[[\\|]]", "(PARA (LINK %5C%7C))"},
		{"[[\\||a]]", "(PARA (LINK a |))"},
		{"[[b\\||a]]", "(PARA (LINK a b|))"},
		{"[[b\\|c|a]]", "(PARA (LINK a b|c))"},
		{"[[\\]]]", "(PARA (LINK %5C%5D))"},
		{"[[\\]|a]]", "(PARA (LINK a ]))"},
		{"[[b\\]|a]]", "(PARA (LINK a b]))"},
		{"[[\\]\\||a]]", "(PARA (LINK a ]|))"},
		{"[[http://a]]", "(PARA (LINK http://a))"},
		{"[[http://a|http://a]]", "(PARA (LINK http://a http://a))"},
		{"[[[[a]]]]", "(PARA [[ (LINK a) ]])"},
		{"[[query:title]]", "(PARA (LINK query:title))"},
		{"[[query:title syntax]]", "(PARA (LINK query:title syntax))"},
		{"[[query:title | action]]", "(PARA (LINK query:title | action))"},
		{"[[Text|query:title]]", "(PARA (LINK query:title Text))"},
		{"[[Text|query:title syntax]]", "(PARA (LINK query:title syntax Text))"},
		{"[[Text|query:title | action]]", "(PARA (LINK query:title | action Text))"},
	})
}

func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[@", "(PARA [@)"},
		{"[@]", "(PARA [@])"},
		{"[@a]", "(PARA (CITE a))"},
		{"[@ a]", "(PARA [@ SP 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))"},
		{"[@a|n ]", "(PARA (CITE a n))"},
		{"[@a,[@b]]", "(PARA (CITE a (CITE b)))"},
		{"[@a]{color=green}", "(PARA (CITE a)[ATTR color=green])"},
	})
}

func TestFootnote(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[^", "(PARA [^)"},
		{"[^]", "(PARA (FN))"},
		{"[^abc]", "(PARA (FN abc))"},
		{"[^abc ]", "(PARA (FN abc))"},
		{"[^abc\ndef]", "(PARA (FN abc SB def))"},
		{"[^abc\n\ndef]", "(PARA [^abc)(PARA def])"},
		{"[^abc[^def]]", "(PARA (FN abc (FN def)))"},
		{"[^abc]{-}", "(PARA (FN abc)[ATTR -])"},
	})
}

func TestImage(t *testing.T) {
func TestEmbed(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"{", "(PARA {)"},
		{"{{", "(PARA {{)"},
		{"{{|", "(PARA {{|)"},
		{"{{}", "(PARA {{})"},
		{"{{|}", "(PARA {{|})"},
		{"{{}}", "(PARA {{}})"},
		{"{{|}}", "(PARA {{|}})"},
		{"{{ }}", "(PARA {{ SP }})"},
		{"{{\n}}", "(PARA {{ SB }})"},
		{"{{a }}", "(PARA {{a SP }})"},
		{"{{a\n}}", "(PARA {{a SB }})"},
		{"{{a}}", "(PARA (IMAGE a))"},
		{"{{12345678901234}}", "(PARA (IMAGE 12345678901234))"},
		{"{{ a}}", "(PARA (IMAGE a))"},
		{"{{a}}", "(PARA (EMBED a))"},
		{"{{12345678901234}}", "(PARA (EMBED 12345678901234))"},
		{"{{ a}}", "(PARA (EMBED a))"},
		{"{{a}", "(PARA {{a})"},
		{"{{|a}}", "(PARA {{|a}})"},
		{"{{b|}}", "(PARA {{b|}})"},
		{"{{b|a}}", "(PARA (IMAGE a b))"},
		{"{{b| a}}", "(PARA (IMAGE a b))"},
		{"{{b|a}}", "(PARA (EMBED a b))"},
		{"{{b| a}}", "(PARA (EMBED a b))"},
		{"{{b|a}", "(PARA {{b|a})"},
		{"{{b\nc|a}}", "(PARA (IMAGE a b SB c))"},
		{"{{b c|a#n}}", "(PARA (IMAGE a#n b SP c))"},
		{"{{a}}{go}", "(PARA (IMAGE a)[ATTR go])"},
		{"{{{{a}}|b}}", "(PARA (IMAGE %7B%7Ba) |b}})"},
	})
}

		{"{{b\nc|a}}", "(PARA (EMBED a b SB c))"},
		{"{{b c|a#n}}", "(PARA (EMBED a#n b SP 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|))"},
func TestTag(t *testing.T) {
	checkTcs(t, TestCases{
		{"#", "(PARA #)"},
		{"##", "(PARA ##)"},
		{"###", "(PARA ###)"},
		{"#tag", "(PARA #tag#)"},
		{"#tag,", "(PARA #tag# ,)"},
		{"#t-g ", "(PARA #t-g#)"},
		{"#t_g", "(PARA #t_g#)"},
		{"{{b\\|c|a}}", "(PARA (EMBED a b|c))"},
		{"{{\\}}}", "(PARA (EMBED %5C%7D))"},
		{"{{\\}|a}}", "(PARA (EMBED a }))"},
		{"{{b\\}|a}}", "(PARA (EMBED a b}))"},
		{"{{\\}\\||a}}", "(PARA (EMBED a }|))"},
		{"{{http://a}}", "(PARA (EMBED http://a))"},
		{"{{http://a|http://a}}", "(PARA (EMBED http://a http://a))"},
		{"{{{{a}}}}", "(PARA {{ (EMBED a) }})"},
	})
}

func TestMark(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[!", "(PARA [!)"},
		{"[!\n", "(PARA [!)"},
		{"[!]", "(PARA (MARK *))"},
		{"[!]", "(PARA (MARK #*))"},
		{"[!][!]", "(PARA (MARK #*) (MARK #*-1))"},
		{"[! ]", "(PARA [! SP ])"},
		{"[!a]", "(PARA (MARK a))"},
		{"[!a]", "(PARA (MARK \"a\" #a))"},
		{"[!a][!a]", "(PARA (MARK \"a\" #a) (MARK \"a\" #a-1))"},
		{"[!a ]", "(PARA [!a SP ])"},
		{"[!a_]", "(PARA (MARK a_))"},
		{"[!a-b]", "(PARA (MARK a-b))"},
		{"[!a][!a]", "(PARA (MARK a) (MARK a-1))"},
		{"[!][!]", "(PARA (MARK *) (MARK *-1))"},
		{"[!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))"},
	})
}

func TestComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"%", "(PARA %)"},
		{"%%", "(PARA {%})"},
		{"%\n", "(PARA %)"},
		{"%%\n", "(PARA {%})"},
		{"%%a", "(PARA {% a})"},
		{"%%%a", "(PARA {% a})"},
		{"%% a", "(PARA {% a})"},
		{"%%%  a", "(PARA {% a})"},
		{"%% % a", "(PARA {% % a})"},
		{"%%a", "(PARA {% a})"},
		{"a%%b", "(PARA a {% b})"},
		{"a %%b", "(PARA a SP {% b})"},
		{"a %%b", "(PARA a {% b})"},
		{" %%b", "(PARA {% b})"},
		{"%%b ", "(PARA {% b })"},
		{"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{"_", "*", ">", "~", "^", ",", "\"", "#", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},
			{"$$\\$", "(PARA $$$)"},
			{"$$a\\$$", "(PARA $$a$$)"},
			{"$$a$\\$", "(PARA $$a$$)"},
			{"$$a\\$$$", "(PARA {$ a$})"},
			{"$$a\na$$", "(PARA {$ a SB a})"},
			{"$$a\n\na$$", "(PARA $$a)(PARA a$$)"},
			{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
		}))
	}
	checkTcs(t, TestCases{
		{"//****//", "(PARA {/ {*}})"},
		{"//**a**//", "(PARA {/ {* a}})"},
		{"//**//**", "(PARA // {* //})"},
		{"__****__", "(PARA {_ {*}})"},
		{"__**a**__", "(PARA {_ {* a}})"},
		{"__**__**", "(PARA __ {* __})"},
	})
}

func TestLiteral(t *testing.T) {
	t.Parallel()
	for _, ch := range []string{"`", "+", "="} {
	for _, ch := range []string{"@", "`", "'", "="} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},
			{"$$\\$", "(PARA $$$)"},
			{"$$a\\$$", "(PARA $$a$$)"},
			{"$$a$\\$", "(PARA $$a$$)"},
			{"$$a\\$$$", "(PARA {$ a$})"},
			{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
		}))
	}
	checkTcs(t, TestCases{
		{"''````''", "(PARA {' ````})"},
		{"''``a``''", "(PARA {' ``a``})"},
		{"''``''``", "(PARA {' ``} ``)"},
		{"''\\'''", "(PARA {' '})"},
	})
}

func TestLiteralMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"++````++", "(PARA {+ ````})"},
		{"++``a``++", "(PARA {+ ``a``})"},
		{"++``++``", "(PARA {+ ``} ``)"},
		{"++\\+++", "(PARA {+ +})"},
		{"$", "(PARA $)"},
		{"$$", "(PARA $$)"},
		{"$$$", "(PARA $$$)"},
		{"$$$$", "(PARA {$})"},
		{"$$a$$", "(PARA {$ a})"},
		{"$$a$$$", "(PARA {$ a} $)"},
		{"$$$a$$", "(PARA {$ $a})"},
		{"$$$a$$$", "(PARA {$ $a} $)"},
		{`$\$`, "(PARA $$)"},
		{`$\$$`, "(PARA $$$)"},
		{`$$\$`, "(PARA $$$)"},
		{`$$a\$$`, `(PARA {$ a\})`},
		{`$$a$\$`, "(PARA $$a$$)"},
		{`$$a\$$$`, `(PARA {$ a\} $)`},
		{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
	})
}

func TestMixFormatCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"//abc//\n**def**", "(PARA {/ abc} SB {* def})"},
		{"++abc++\n==def==", "(PARA {+ abc} SB {= def})"},
		{"//abc//\n==def==", "(PARA {/ abc} SB {= def})"},
		{"//abc//\n``def``", "(PARA {/ abc} SB {` def})"},
		{"__abc__\n**def**", "(PARA {_ abc} SB {* def})"},
		{"''abc''\n==def==", "(PARA {' abc} SB {= def})"},
		{"__abc__\n==def==", "(PARA {_ abc} SB {= def})"},
		{"__abc__\n``def``", "(PARA {_ abc} SB {` def})"},
		{"\"\"ghi\"\"\n::abc::\n``def``\n", "(PARA {\" ghi} SB {: abc} SB {` def})"},
	})
}

func TestNDash(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"--", "(PARA \u2013)"},
		{"a--b", "(PARA a\u2013b)"},
	})
}

func TestEntity(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"&", "(PARA &)"},
		{"&;", "(PARA &;)"},
		{"&#;", "(PARA &#;)"},
		{"&#1a;", "(PARA & #1a# ;)"},
		{"&#x;", "(PARA & #x# ;)"},
		{"&#x0z;", "(PARA & #x0z# ;)"},
		{"&#1a;", "(PARA &#1a;)"},
		{"&#x;", "(PARA &#x;)"},
		{"&#x0z;", "(PARA &#x0z;)"},
		{"&1;", "(PARA &1;)"},
		{"&#9;", "(PARA &#9;)"}, // No numeric entities below space are not allowed.
		{"&#x1f;", "(PARA &#x1f;)"},

		// Good cases
		{"&lt;", "(PARA <)"},
		{"&#48;", "(PARA 0)"},
		{"&#x4A;", "(PARA J)"},
		{"&#X4a;", "(PARA J)"},
		{"&hellip;", "(PARA \u2026)"},
		{"&nbsp;", "(PARA \u00a0)"},
		{"E: &amp;,&#13;;&#xa;.", "(PARA E: SP &,\r;\n.)"},
		{"E: &amp;,&#63;;&#x63;.", "(PARA E: SP &,?;c.)"},
	})
}

func TestVerbatimZettel(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"@@@\n@@@", "(ZETTEL)"},
		{"@@@\nabc\n@@@", "(ZETTEL\nabc)"},
		{"@@@@def\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =def]"},
	})
}

func TestVerbatim(t *testing.T) {
func TestVerbatimCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"```\n```", "(PROG)"},
		{"```\nabc\n```", "(PROG\nabc)"},
		{"```\nabc\n````", "(PROG\nabc)"},
		{"````\nabc\n````", "(PROG\nabc)"},
		{"````\nabc\n```\n````", "(PROG\nabc\n```)"},
		{"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"},
	})
}

func TestVerbatimEval(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"~~~\n~~~", "(EVAL)"},
		{"~~~\nabc\n~~~", "(EVAL\nabc)"},
		{"~~~\nabc\n~~~~", "(EVAL\nabc)"},
		{"~~~~\nabc\n~~~~", "(EVAL\nabc)"},
		{"~~~~\nabc\n~~~\n~~~~", "(EVAL\nabc\n~~~)"},
		{"~~~~go\nabc\n~~~~", "(EVAL\nabc)[ATTR =go]"},
	})
}

func TestVerbatimMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"$$$\n$$$", "(MATH)"},
		{"$$$\nabc\n$$$", "(MATH\nabc)"},
		{"$$$\nabc\n$$$$", "(MATH\nabc)"},
		{"$$$$\nabc\n$$$$", "(MATH\nabc)"},
		{"$$$$\nabc\n$$$\n$$$$", "(MATH\nabc\n$$$)"},
		{"$$$$go\nabc\n$$$$", "(MATH\nabc)[ATTR =go]"},
	})
}

func TestVerbatimComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"%%%\n%%%", "(COMMENT)"},
		{"%%%\nabc\n%%%", "(COMMENT\nabc)"},
		{"%%%%go\nabc\n%%%%", "(COMMENT\nabc)[ATTR =go]"},
	})
}

func TestPara(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"a\n\nb", "(PARA a)(PARA b)"},
		{"a\n \nb", "(PARA a)(PARA b)"},
	})
}

func TestSpanRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{":::\n:::", "(SPAN)"},
		{":::\nabc\n:::", "(SPAN (PARA abc))"},
		{":::\nabc\n::::", "(SPAN (PARA abc))"},
		{"::::\nabc\n::::", "(SPAN (PARA abc))"},
		{"::::\nabc\n:::\ndef\n:::\n::::", "(SPAN (PARA abc)(SPAN (PARA def)))"},
		{":::{go}\n:::", "(SPAN)[ATTR go]"},
		{":::\nabc\n::: def ", "(SPAN (PARA abc) (LINE def))"},
	})
}

func TestQuoteRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"<<<\n<<<", "(QUOTE)"},
		{"<<<\nabc\n<<<", "(QUOTE (PARA abc))"},
		{"<<<\nabc\n<<<<", "(QUOTE (PARA abc))"},
		{"<<<<\nabc\n<<<<", "(QUOTE (PARA abc))"},
		{"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(QUOTE (PARA abc)(QUOTE (PARA def)))"},
		{"<<<go\n<<<", "(QUOTE)[ATTR =go]"},
		{"<<<\nabc\n<<< def ", "(QUOTE (PARA abc) (LINE def))"},
	})
}

func TestVerseRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, replace("\"", TestCases{
		{"$$$\n$$$", "(VERSE)"},
		{"$$$\nabc\n$$$", "(VERSE (PARA abc))"},
		{"$$$\nabc\n$$$$", "(VERSE (PARA abc))"},
		{"$$$$\nabc\n$$$$", "(VERSE (PARA abc))"},
		{"$$$\nabc\ndef\n$$$", "(VERSE (PARA abc HB def))"},
		{"$$$$\nabc\n$$$\ndef\n$$$\n$$$$", "(VERSE (PARA abc)(VERSE (PARA def)))"},
		{"$$$go\n$$$", "(VERSE)[ATTR =go]"},
		{"$$$\nabc\n$$$ def ", "(VERSE (PARA abc) (LINE def))"},
	}))
}

func TestHeading(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"=h", "(PARA =h)"},
		{"= h", "(PARA = SP h)"},
		{"==h", "(PARA ==h)"},
		{"== h", "(PARA == SP h)"},
		{"===h", "(PARA ===h)"},
		{"=== h", "(H2 h)"},
		{"===  h", "(H2 h)"},
		{"==== h", "(H3 h)"},
		{"===== h", "(H4 h)"},
		{"====== h", "(H5 h)"},
		{"======= h", "(H6 h)"},
		{"======== h", "(H6 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//", "(H2 h= {/ =a})"},
		{"=== h=__=a__", "(H1 h= {_ =a} #h-a)"},
		{"=\n", "(PARA =)"},
		{"a=", "(PARA a=)"},
		{" =", "(PARA =)"},
		{"=== h\na", "(H2 h)(PARA a)"},
		{"=== h i {-}", "(H2 h SP i)[ATTR -]"},
		{"=== 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{{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 #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) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"-", "(PARA -)"},
		{"---", "(HR)"},
		{"----", "(HR)"},
		{"---A", "(HR)[ATTR =A]"},
		{"---A-", "(HR)[ATTR =A-]"},
		{"-1", "(PARA -1)"},
		{"2-1", "(PARA 2-1)"},
		{"---  {  go  }  ", "(HR)[ATTR go]"},
		{"---  {  .go  }  ", "(HR)[ATTR class=go]"},
	})
}

func TestList(t *testing.T) {
	t.Parallel()
	// No ">" in the following, because quotation lists may have empty items.
	for _, ch := range []string{"*", "#"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$ ", "(PARA $)"},
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
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







-
+




+
+
+
+
+
+
+
+
+

+







+








		// A HRule creates a new list
		{"* abc\n---\n* def", "(UL {(PARA abc)})(HR)(UL {(PARA def)})"},

		// Changing list type adds a new list
		{"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"},

		// Quotation lists mayx have empty items
		// Quotation lists may have empty items
		{">", "(QL {})"},
	})
}

func TestQuoteList(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"> w1 w2", "(QL {(PARA w1 SP 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()
	checkTcs(t, TestCases{
		{"abc\n* def", "(PARA abc)(UL {(PARA def)})"},
		{"abc\n*def", "(PARA abc SB *def)"},
	})
}

func TestDefinition(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{";", "(PARA ;)"},
		{"; ", "(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))"},
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
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







+


+
+







+
+
+
+
+
+
+
+
+
+
+
+
+
+




+













-
+











-
+









+







		{"; abc\n:", "(DL (DT abc))(PARA :)"},
		{"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"},
		{"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"},
	})
}

func TestTable(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"|", "(TAB (TR))"},
		{"||", "(TAB (TR (TD)))"},
		{"| |", "(TAB (TR (TD)))"},
		{"|a", "(TAB (TR (TD a)))"},
		{"|a|", "(TAB (TR (TD a)))"},
		{"|a| ", "(TAB (TR (TD a)(TD)))"},
		{"|a|b", "(TAB (TR (TD a)(TD b)))"},
		{"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"},
		{"|%", ""},
		{"|a|b\n|%---\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"},
		{"|a|b\n|c", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD)))"},
	})
}

func TestTransclude(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"{{{a}}}", "(TRANSCLUDE a)"},
		{"{{{a}}}b", "(TRANSCLUDE a)[ATTR =b]"},
		{"{{{a}}}}", "(TRANSCLUDE a)"},
		{"{{{a\\}}}}", "(TRANSCLUDE a%5C%7D)"},
		{"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)[ATTR =b]"},
		{"{{{a}}", "(PARA { (EMBED a))"},
		{"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"},
	})
}

func TestBlockAttr(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{":::go\n:::", "(SPAN)[ATTR =go]"},
		{":::go=\n:::", "(SPAN)[ATTR =go]"},
		{":::{}\n:::", "(SPAN)"},
		{":::{ }\n:::", "(SPAN)"},
		{":::{.go}\n:::", "(SPAN)[ATTR class=go]"},
		{":::{=go}\n:::", "(SPAN)[ATTR =go]"},
		{":::{go}\n:::", "(SPAN)[ATTR go]"},
		{":::{go=py}\n:::", "(SPAN)[ATTR go=py]"},
		{":::{.go=py}\n:::", "(SPAN)"},
		{":::{go=}\n:::", "(SPAN)[ATTR go]"},
		{":::{.go=}\n:::", "(SPAN)"},
		{":::{go py}\n:::", "(SPAN)[ATTR go py]"},
		{":::{go\npy}\n:::", "(SPAN (PARA py}))"},
		{":::{go\npy}\n:::", "(SPAN)[ATTR go py]"},
		{":::{.go py}\n:::", "(SPAN)[ATTR class=go py]"},
		{":::{go .py}\n:::", "(SPAN)[ATTR class=py go]"},
		{":::{.go py=3}\n:::", "(SPAN)[ATTR class=go py=3]"},
		{":::  {  go  }  \n:::", "(SPAN)[ATTR go]"},
		{":::  {  .go  }  \n:::", "(SPAN)[ATTR class=go]"},
	})
	checkTcs(t, replace("\"", TestCases{
		{":::{py=3}\n:::", "(SPAN)[ATTR py=3]"},
		{":::{py=$2 3$}\n:::", "(SPAN)[ATTR py=$2 3$]"},
		{":::{py=$2\\$3$}\n:::", "(SPAN)[ATTR py=2$3]"},
		{":::{py=2$3}\n:::", "(SPAN)[ATTR py=2$3]"},
		{":::{py=$2\n3$}\n:::", "(SPAN (PARA 3$}))"},
		{":::{py=$2\n3$}\n:::", "(SPAN)[ATTR py=$2\n3$]"},
		{":::{py=$2 3}\n:::", "(SPAN)"},
		{":::{py=2 py=3}\n:::", "(SPAN)[ATTR py=$2 3$]"},
		{":::{.go .py}\n:::", "(SPAN)[ATTR class=$go py$]"},
		{":::{go go}\n:::", "(SPAN)[ATTR go]"},
		{":::{=py =go}\n:::", "(SPAN)[ATTR =go]"},
	}))
}

func TestInlineAttr(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"::a::{}", "(PARA {: a})"},
		{"::a::{ }", "(PARA {: a})"},
		{"::a::{.go}", "(PARA {: a}[ATTR class=go])"},
		{"::a::{=go}", "(PARA {: a}[ATTR =go])"},
		{"::a::{go}", "(PARA {: a}[ATTR go])"},
		{"::a::{go=py}", "(PARA {: a}[ATTR go=py])"},
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
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
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







-
+








+









-
+


-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+





-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+





-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
-
-
-
-
+
+
+

-
-
-
-
-
-
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
-

-
-
-
-
-
-
-
+
+
-
-
+
-
-
-
-
-
-
+
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
-
+
+
+
-
-
+
-
-
+
-
-
-
-
-
+
+
+
-
-
-

-
+
-
-
-
-
-
-
-
-
+
-
-
-
+
+
-
-
-
-
-
-
+
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
-

		{"::a::{\ngo\n}", "(PARA {: a}[ATTR go])"},
	})
	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 3$])"},
		{"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"},
		{"::a::{py=$2 3}", "(PARA {: a} {py=$2 SP 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 {
	b strings.Builder
	sb strings.Builder
}

func (tv *TestVisitor) String() string { return tv.b.String() }
func (tv *TestVisitor) VisitPara(pn *ast.ParaNode) {
	tv.b.WriteString("(PARA")
	tv.visitInlineSlice(pn.Inlines)
	tv.b.WriteByte(')')
}

var mapVerbatimCode = map[ast.VerbatimCode]string{
	ast.VerbatimProg: "(PROG",
}

func (tv *TestVisitor) VisitVerbatim(vn *ast.VerbatimNode) {
	code, ok := mapVerbatimCode[vn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
	}
	tv.b.WriteString(code)
	for _, line := range vn.Lines {
		tv.b.WriteByte('\n')
		tv.b.WriteString(line)
	}
	tv.b.WriteByte(')')
	tv.visitAttributes(vn.Attrs)
}

var mapRegionCode = map[ast.RegionCode]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.sb.WriteString("(PARA")
		ast.Walk(tv, &n.Inlines)
		tv.sb.WriteByte(')')
	case *ast.VerbatimNode:
		code, ok := mapVerbatimKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind))
		}
		tv.sb.WriteString(code)
		if len(n.Content) > 0 {
			tv.sb.WriteByte('\n')
			tv.sb.Write(n.Content)
		}
		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.sb.WriteString(code)
		if len(n.Blocks) > 0 {
			tv.sb.WriteByte(' ')
			ast.Walk(tv, &n.Blocks)
		}
		if len(n.Inlines) > 0 {
			tv.sb.WriteString(" (LINE")
			ast.Walk(tv, &n.Inlines)
			tv.sb.WriteByte(')')
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HeadingNode:
		fmt.Fprintf(&tv.sb, "(H%d", n.Level)
		ast.Walk(tv, &n.Inlines)
		if n.Fragment != "" {
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HRuleNode:
		tv.sb.WriteString("(HR)")
		tv.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		tv.sb.WriteString(mapNestedListKind[n.Kind])
		for _, item := range n.Items {
			tv.sb.WriteString(" {")
			ast.WalkItemSlice(tv, item)
			tv.sb.WriteByte('}')
		}
		tv.sb.WriteByte(')')
	case *ast.DescriptionListNode:
		tv.sb.WriteString("(DL")
		for _, def := range n.Descriptions {
			tv.sb.WriteString(" (DT")
			ast.Walk(tv, &def.Term)
			tv.sb.WriteByte(')')
			for _, b := range def.Descriptions {
				tv.sb.WriteString(" (DD ")
				ast.WalkDescriptionSlice(tv, b)
				tv.sb.WriteByte(')')
			}
		}
		tv.sb.WriteByte(')')
	case *ast.TableNode:
		tv.sb.WriteString("(TAB")
		if len(n.Header) > 0 {
			tv.sb.WriteString(" (TR")
			for _, cell := range n.Header {
				tv.sb.WriteString(" (TH")
				tv.sb.WriteString(alignString[cell.Align])
				ast.Walk(tv, &cell.Inlines)
				tv.sb.WriteString(")")
			}
			tv.sb.WriteString(")")
		}
		if len(n.Rows) > 0 {
			tv.sb.WriteString(" ")
			for _, row := range n.Rows {
				tv.sb.WriteString("(TR")
				for i, cell := range row {
					if i == 0 {
						tv.sb.WriteString(" ")
					}
					tv.sb.WriteString("(TD")
					tv.sb.WriteString(alignString[cell.Align])
					ast.Walk(tv, &cell.Inlines)
					tv.sb.WriteString(")")
				}
				tv.sb.WriteString(")")
			}
		}
		tv.sb.WriteString(")")
	case *ast.TranscludeNode:
		fmt.Fprintf(&tv.sb, "(TRANSCLUDE %v)", n.Ref)
		tv.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		tv.sb.WriteString("(BLOB ")
		tv.sb.WriteString(n.Syntax)
		tv.sb.WriteString(")")
	case *ast.TextNode:
		tv.sb.WriteString(n.Text)
	case *ast.SpaceNode:
		if l := n.Count(); l == 1 {
			tv.sb.WriteString("SP")
		} else {
			fmt.Fprintf(&tv.sb, "SP%d", l)
		}
	case *ast.BreakNode:
		if n.Hard {
			tv.sb.WriteString("HB")
		} else {
			tv.sb.WriteString("SB")
		}
	case *ast.LinkNode:
		fmt.Fprintf(&tv.sb, "(LINK %v", n.Ref)
		ast.Walk(tv, &n.Inlines)
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedRefNode:
		fmt.Fprintf(&tv.sb, "(EMBED %v", n.Ref)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedBLOBNode:
		panic("TODO: zmktest blob")
	case *ast.CiteNode:
		fmt.Fprintf(&tv.sb, "(CITE %s", n.Key)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.FootnoteNode:
		tv.sb.WriteString("(FN")
		ast.Walk(tv, &n.Inlines)
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		tv.sb.WriteString("(MARK")
		if n.Mark != "" {
			tv.sb.WriteString(" \"")
			tv.sb.WriteString(n.Mark)
			tv.sb.WriteByte('"')
		}
		if n.Fragment != "" {
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.sb.WriteByte(')')
	case *ast.FormatNode:
		fmt.Fprintf(&tv.sb, "{%c", mapFormatKind[n.Kind])
		ast.Walk(tv, &n.Inlines)
		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.sb.WriteByte('{')
		tv.sb.WriteRune(code)
		if len(n.Content) > 0 {
			tv.sb.WriteByte(' ')
			tv.sb.Write(n.Content)
		}
		tv.sb.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	default:
		return tv
	}
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "(ZETTEL",
	ast.VerbatimProg:    "(PROG",
	ast.VerbatimEval:    "(EVAL",
	ast.VerbatimMath:    "(MATH",
	ast.VerbatimComment: "(COMMENT",
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  "(SPAN",
	ast.RegionQuote: "(QUOTE",
	ast.RegionVerse: "(VERSE",
}

// VisitRegion stores information about a region.
func (tv *TestVisitor) VisitRegion(rn *ast.RegionNode) {
	code, ok := mapRegionCode[rn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown region code %v", rn.Code))
	}
	tv.b.WriteString(code)
	if rn.Blocks != nil {
		tv.b.WriteByte(' ')
		tv.visitBlockSlice(rn.Blocks)
	}
	if len(rn.Inlines) > 0 {
		tv.b.WriteString(" (LINE")
		tv.visitInlineSlice(rn.Inlines)
		tv.b.WriteByte(')')
	}
	tv.b.WriteByte(')')
	tv.visitAttributes(rn.Attrs)
}

func (tv *TestVisitor) VisitHeading(hn *ast.HeadingNode) {
	fmt.Fprintf(&tv.b, "(H%d", hn.Level)
	tv.visitInlineSlice(hn.Inlines)
	tv.b.WriteByte(')')
	tv.visitAttributes(hn.Attrs)
}
func (tv *TestVisitor) VisitHRule(hn *ast.HRuleNode) {
	tv.b.WriteString("(HR)")
	tv.visitAttributes(hn.Attrs)
}

var mapNestedListCode = map[ast.NestedListCode]string{
var mapNestedListKind = map[ast.NestedListKind]string{
	ast.NestedListOrdered:   "(OL",
	ast.NestedListUnordered: "(UL",
	ast.NestedListQuote:     "(QL",
}

func (tv *TestVisitor) VisitNestedList(ln *ast.NestedListNode) {
	tv.b.WriteString(mapNestedListCode[ln.Code])
	for _, item := range ln.Items {
		tv.b.WriteString(" {")
		tv.visitItemSlice(item)
		tv.b.WriteByte('}')
	}
	tv.b.WriteByte(')')
}
func (tv *TestVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	tv.b.WriteString("(DL")
	for _, def := range dn.Descriptions {
		tv.b.WriteString(" (DT")
		tv.visitInlineSlice(def.Term)
		tv.b.WriteByte(')')
		for _, b := range def.Descriptions {
			tv.b.WriteString(" (DD ")
			tv.visitDescriptionSlice(b)
			tv.b.WriteByte(')')
		}
	}
	tv.b.WriteByte(')')
}

var alignString = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "l",
	ast.AlignCenter:  "c",
	ast.AlignRight:   "r",
}

// VisitTable emits a HTML table.
func (tv *TestVisitor) VisitTable(tn *ast.TableNode) {
	tv.b.WriteString("(TAB")
	if len(tn.Header) > 0 {
		tv.b.WriteString(" (TR")
		for _, cell := range tn.Header {
			tv.b.WriteString(" (TH")
			tv.b.WriteString(alignString[cell.Align])
			tv.visitInlineSlice(cell.Inlines)
			tv.b.WriteString(")")
		}
var mapFormatKind = map[ast.FormatKind]rune{
	ast.FormatEmph:   '_',
	ast.FormatStrong: '*',
	ast.FormatInsert: '>',
	ast.FormatDelete: '~',
	ast.FormatSuper:  '^',
	ast.FormatSub:    ',',
	ast.FormatQuote:  '"',
	ast.FormatMark:   '#',
	ast.FormatSpan:   ':',
}
		tv.b.WriteString(")")
	}

	if len(tn.Rows) > 0 {
		tv.b.WriteString(" ")
		for _, row := range tn.Rows {
			tv.b.WriteString("(TR")
			for i, cell := range row {
				if i == 0 {
					tv.b.WriteString(" ")
				}
				tv.b.WriteString("(TD")
				tv.b.WriteString(alignString[cell.Align])
				tv.visitInlineSlice(cell.Inlines)
				tv.b.WriteString(")")
			}
			tv.b.WriteString(")")
		}
	}
	tv.b.WriteString(")")
}

var mapLiteralKind = map[ast.LiteralKind]rune{
func (tv *TestVisitor) VisitBLOB(bn *ast.BLOBNode) {
	tv.b.WriteString("(BLOB ")
	tv.b.WriteString(bn.Syntax)
	tv.b.WriteString(")")
	ast.LiteralZettel:  '@',
	ast.LiteralProg:    '`',
	ast.LiteralInput:   '\'',
}

func (tv *TestVisitor) VisitText(tn *ast.TextNode) {
	tv.b.WriteString(tn.Text)
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
	ast.LiteralMath:    '$',
}
func (tv *TestVisitor) VisitTag(tn *ast.TagNode) {
	tv.b.WriteByte('#')
	tv.b.WriteString(tn.Tag)
	tv.b.WriteByte('#')
}
func (tv *TestVisitor) VisitSpace(sn *ast.SpaceNode) {

func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) {
	if len(sn.Lexeme) == 1 {
		tv.b.WriteString("SP")
	} else {
	for _, in := range *is {
		fmt.Fprintf(&tv.b, "SP%d", len(sn.Lexeme))
	}
}
func (tv *TestVisitor) VisitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		tv.b.WriteString("HB")
	} else {
		tv.b.WriteString("SB")
	}
}
func (tv *TestVisitor) VisitLink(tn *ast.LinkNode) {
	fmt.Fprintf(&tv.b, "(LINK %s", tn.Ref)
	tv.visitInlineSlice(tn.Inlines)
	tv.b.WriteByte(')')
		tv.sb.WriteByte(' ')
	tv.visitAttributes(tn.Attrs)
}
func (tv *TestVisitor) VisitImage(in *ast.ImageNode) {
	fmt.Fprintf(&tv.b, "(IMAGE %s", in.Ref)
		ast.Walk(tv, in)
	tv.visitInlineSlice(in.Inlines)
	tv.b.WriteByte(')')
	tv.visitAttributes(in.Attrs)
}
	}
func (tv *TestVisitor) VisitCite(cn *ast.CiteNode) {
	fmt.Fprintf(&tv.b, "(CITE %s", cn.Key)
	tv.visitInlineSlice(cn.Inlines)
	tv.b.WriteByte(')')
	tv.visitAttributes(cn.Attrs)
}
func (tv *TestVisitor) VisitFootnote(fn *ast.FootnoteNode) {
	tv.b.WriteString("(FN")
	tv.visitInlineSlice(fn.Inlines)
	tv.b.WriteByte(')')
	tv.visitAttributes(fn.Attrs)
}
func (tv *TestVisitor) VisitMark(mn *ast.MarkNode) {

func (tv *TestVisitor) visitAttributes(a attrs.Attributes) {
	tv.b.WriteString("(MARK")
	if len(mn.Text) > 0 {
	if a.IsEmpty() {
		tv.b.WriteByte(' ')
		tv.b.WriteString(mn.Text)
	}
	tv.b.WriteByte(')')
}

		return
	}
var mapCode = map[ast.FormatCode]rune{
	ast.FormatItalic:    '/',
	ast.FormatBold:      '*',
	ast.FormatUnder:     '_',
	ast.FormatStrike:    '~',
	tv.sb.WriteString("[ATTR")
	ast.FormatMonospace: '\'',
	ast.FormatSuper:     '^',
	ast.FormatSub:       ',',
	ast.FormatQuote:     '"',
	ast.FormatQuotation: '<',
	ast.FormatSmall:     ';',
	ast.FormatSpan:      ':',
}


	for _, k := range a.Keys() {
func (tv *TestVisitor) VisitFormat(fn *ast.FormatNode) {
	fmt.Fprintf(&tv.b, "{%c", mapCode[fn.Code])
	tv.visitInlineSlice(fn.Inlines)
	tv.b.WriteByte('}')
		tv.sb.WriteByte(' ')
	tv.visitAttributes(fn.Attrs)
}

		tv.sb.WriteString(k)
var mapLiteralCode = map[ast.LiteralCode]rune{
	ast.LiteralProg:    '`',
	ast.LiteralKeyb:    '+',
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
}

		v := a[k]
func (tv *TestVisitor) VisitLiteral(ln *ast.LiteralNode) {
	code, ok := mapLiteralCode[ln.Code]
	if !ok {
		if len(v) > 0 {
		panic(fmt.Sprintf("No element for code %v", ln.Code))
	}
	tv.b.WriteByte('{')
			tv.sb.WriteByte('=')
	tv.b.WriteRune(code)
	if len(ln.Text) > 0 {
		tv.b.WriteByte(' ')
		tv.b.WriteString(ln.Text)
			if quoteString(v) {
				tv.sb.WriteByte('"')
				tv.sb.WriteString(v)
	}
	tv.b.WriteByte('}')
				tv.sb.WriteByte('"')
	tv.visitAttributes(ln.Attrs)
}
			} else {
func (tv *TestVisitor) visitBlockSlice(bns ast.BlockSlice) {
	for _, bn := range bns {
		bn.Accept(tv)
	}
}
				tv.sb.WriteString(v)
			}
		}
func (tv *TestVisitor) visitItemSlice(ins ast.ItemSlice) {
	for _, in := range ins {
		in.Accept(tv)
	}
}

func (tv *TestVisitor) visitDescriptionSlice(dns ast.DescriptionSlice) {
	for _, dn := range dns {
		dn.Accept(tv)
	}
}
func (tv *TestVisitor) visitInlineSlice(ins ast.InlineSlice) {
	for _, in := range ins {
		tv.b.WriteByte(' ')
	tv.sb.WriteByte(']')
		in.Accept(tv)
	}
}
}

func (tv *TestVisitor) visitAttributes(a *ast.Attributes) {
	if a == nil || len(a.Attrs) == 0 {
		return
	}
	tv.b.WriteString("[ATTR")

func quoteString(s string) bool {
	keys := make([]string, 0, len(a.Attrs))
	for k := range a.Attrs {
	for _, ch := range s {
		keys = append(keys, k)
	}
	sort.Strings(keys)

		if ch <= ' ' {
	for _, k := range keys {
		tv.b.WriteByte(' ')
		tv.b.WriteString(k)
		v := a.Attrs[k]
			return true
		if len(v) > 0 {
			tv.b.WriteByte('=')
			if strings.ContainsRune(v, ' ') {
				tv.b.WriteByte('"')
				tv.b.WriteString(v)
				tv.b.WriteByte('"')
			} else {
				tv.b.WriteString(v)
			}
		}
		}
	}
	}

	return false
	tv.b.WriteByte(']')
}

Deleted place/constplace/base.css.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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























































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
*,*::before,*::after {
    box-sizing: border-box;
  }
  html {
    font-size: 1rem;
    font-family: serif;
    scroll-behavior: smooth;
    height: 100%;
  }
  body {
    margin: 0;
    min-height: 100vh;
    text-rendering: optimizeSpeed;
    line-height: 1.4;
    overflow-x: hidden;
    background-color: #f8f8f8 ;
    height: 100%;
  }
  nav.zs-menu {
    background-color: hsl(210, 28%, 90%);
    overflow: auto;
    white-space: nowrap;
    font-family: sans-serif;
    padding-left: .5rem;
  }
  nav.zs-menu > a {
    float:left;
    display: block;
    text-align: center;
    padding:.41rem .5rem;
    text-decoration: none;
    color:black;
  }
  nav.zs-menu > a:hover, .zs-dropdown:hover button {
    background-color: hsl(210, 28%, 80%);
  }
  nav.zs-menu form {
    float: right;
  }
  nav.zs-menu form input[type=text] {
    padding: .12rem;
    border: none;
    margin-top: .25rem;
    margin-right: .5rem;
  }
  .zs-dropdown {
    float: left;
    overflow: hidden;
  }
  .zs-dropdown > button {
    font-size: 16px;
    border: none;
    outline: none;
    color: black;
    padding:.41rem .5rem;
    background-color: inherit;
    font-family: inherit;
    margin: 0;
  }
  .zs-dropdown-content {
    display: none;
    position: absolute;
    background-color: #f9f9f9;
    min-width: 160px;
    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
    z-index: 1;
  }
  .zs-dropdown-content > a {
    float: none;
    color: black;
    padding:.41rem .5rem;
    text-decoration: none;
    display: block;
    text-align: left;
  }
  .zs-dropdown-content > a:hover {
    background-color: hsl(210, 28%, 75%);
  }
  .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;
  }
  ol,ul {
    padding-left: 1.1rem;
  }
  li,figure,figcaption,dl {
    margin: 0;
  }
  dt {
    margin: .5rem 0 0 0;
  }
  dt+dd {
    margin-top: 0;
  }
  dd {
    margin: .5rem 0 0 2rem;
  }
  dd > p:first-child {
    margin: 0 0 0 0;
  }
  blockquote {
    border-left: 0.5rem solid lightgray;
    padding-left: 1rem;
    margin-left: 1rem;
    margin-right: 2rem;
    font-style: italic;
  }
  blockquote p {
    margin-bottom: .5rem;
  }
  blockquote cite {
    font-style: normal;
  }
  table {
    border-collapse: collapse;
    border-spacing: 0;
    max-width: 100%;
  }
  th,td {
    text-align: left;
    padding: .25rem .5rem;
  }
  td { border-bottom: 1px solid hsl(0, 0%, 85%); }
  thead th { border-bottom: 2px solid hsl(0, 0%, 70%); }
  tfoot th { border-top: 2px solid hsl(0, 0%, 70%); }
  main form {
    padding: 0 .5em;
    margin: .5em 0 0 0;
  }
  main form:after {
    content: ".";
    display: block;
    height: 0;
    clear: both;
    visibility: hidden;
  }
  main form div {
    margin: .5em 0 0 0
  }
  input {
    font-family: monospace;
  }
  input[type="submit"],button,select {
    font: inherit;
  }
  label { font-family: sans-serif; font-size:.9rem }
  label::after { content:":" }
  textarea {
    font-family: monospace;
    resize: vertical;
    width: 100%;
  }
  .zs-input {
    padding: .5em;
    display:block;
    border:none;
    border-bottom:1px solid #ccc;
    width:100%;
  }
  .zs-button {
    float:right;
    margin: .5em 0 .5em 1em;
  }
  a:not([class]) {
    text-decoration-skip-ink: auto;
  }
  .zs-broken {
    text-decoration: line-through;
  }
  img {
    max-width: 100%;
  }
  .zs-endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  code,pre,kbd {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1rem .2rem;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25rem;
  }
  pre {
    padding: .5rem .7rem;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5rem;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5rem .7rem;
    max-width: 100%;
    border-radius: .5rem;
    border: 1px solid black;
  }
  div.zs-indication p:first-child {
    margin-top: 0;
  }
  span.zs-indication {
    border: 1px solid black;
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 95%;
  }
  .zs-example { border-style: dotted !important }
  .zs-error {
    background-color: lightpink;
    border-style: none !important;
    font-weight: bold;
  }
  kbd {
    background: hsl(210, 5%, 100%);
    border: 1px solid hsl(210, 5%, 70%);
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 75%;
  }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1rem;
  }
  .zs-meta a {
    color:#444;
  }
  h1+.zs-meta {
    margin-top:-1rem;
  }
  details > summary {
    width: 100%;
    background-color: #eee;
    font-family:sans-serif;
  }
  details > ul {
    margin-top:0;
    padding-left:2rem;
    background-color: #eee;
  }
  footer {
    padding: 0 1rem;
  }
  @media (prefers-reduced-motion: reduce) {
    * {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }

Deleted place/constplace/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

































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<!DOCTYPE html>
<html{{#Lang}} lang="{{Lang}}"{{/Lang}}>
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<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="{{{StylesheetURL}}}">
<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="{{{UserLogoutURL}}}">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>
</nav>
</div>
{{#CanCreate}}
<div class="zs-dropdown">
<button>New</button>
<nav class="zs-dropdown-content">
{{#NewZettelLinks}}
<a href="{{{URL}}}">{{Text}}</a>
{{/NewZettelLinks}}
</nav>
</div>
{{/CanCreate}}
<form action="{{{SearchURL}}}">
<input type="text" placeholder="Search.." name="s">
</form>
</nav>
<main class="content">
{{{Content}}}
</main>
{{#FooterHTML}}
<footer>
{{{FooterHTML}}}
</footer>
{{/FooterHTML}}
</body>
</html>

Deleted place/constplace/constplace.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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





































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package constplace places zettel inside the executable.
package constplace

import (
	"context"
	_ "embed" // Allow to embed file content
	"net/url"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"
	"zettelstore.de/z/search"
)

func init() {
	manager.Register(
		" const",
		func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) {
			return &constPlace{zettel: constZettelMap, enricher: cdata.Enricher}, nil
		})
}

type constHeader map[string]string

func makeMeta(zid id.Zid, h constHeader) *meta.Meta {
	m := meta.New(zid)
	for k, v := range h {
		m.Set(k, v)
	}
	return m
}

type constZettel struct {
	header  constHeader
	content domain.Content
}

type constPlace struct {
	zettel   map[id.Zid]constZettel
	enricher place.Enricher
}

func (cp *constPlace) Location() string {
	return "const:"
}

func (cp *constPlace) CanCreateZettel(ctx context.Context) bool { return false }

func (cp *constPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	return id.Invalid, place.ErrReadOnly
}

func (cp *constPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	if z, ok := cp.zettel[zid]; ok {
		return domain.Zettel{Meta: makeMeta(zid, z.header), Content: z.content}, nil
	}
	return domain.Zettel{}, place.ErrNotFound
}

func (cp *constPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	if z, ok := cp.zettel[zid]; ok {
		return makeMeta(zid, z.header), nil
	}
	return nil, place.ErrNotFound
}

func (cp *constPlace) FetchZids(ctx context.Context) (id.Set, error) {
	result := id.NewSetCap(len(cp.zettel))
	for zid := range cp.zettel {
		result[zid] = true
	}
	return result, nil
}

func (cp *constPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
	for zid, zettel := range cp.zettel {
		m := makeMeta(zid, zettel.header)
		cp.enricher.Enrich(ctx, m)
		if match(m) {
			res = append(res, m)
		}
	}
	return res, nil
}

func (cp *constPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	return false
}

func (cp *constPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	return place.ErrReadOnly
}

func (cp *constPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	_, ok := cp.zettel[zid]
	return !ok
}

func (cp *constPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if _, ok := cp.zettel[curZid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}
func (cp *constPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false }

func (cp *constPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := cp.zettel[zid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}

func (cp *constPlace) ReadStats(st *place.ManagedPlaceStats) {
	st.ReadOnly = true
	st.Zettel = len(cp.zettel)
}

const syntaxTemplate = "mustache"

var constZettelMap = map[id.Zid]constZettel{
	id.ConfigurationZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Runtime Configuration",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityOwner,
			meta.KeySyntax:     meta.ValueSyntaxNone,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		""},
	id.LicenseZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore License",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeySyntax:     meta.ValueSyntaxText,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
		},
		domain.NewContent(contentLicense)},
	id.AuthorsZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Contributors",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
		},
		domain.NewContent(contentContributors)},
	id.DependenciesZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Dependencies",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
		},
		domain.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentBaseMustache)},
	id.LoginTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentLoginMustache)},
	id.ZettelTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentZettelMustache)},
	id.InfoTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentInfoMustache)},
	id.ContextTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Context HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentContextMustache)},
	id.FormTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentFormMustache)},
	id.RenameTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Rename Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentRenameMustache)},
	id.DeleteTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentDeleteMustache)},
	id.ListTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentListZettelMustache)},
	id.RolesTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Roles HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentListRolesMustache)},
	id.TagsTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Tags HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentListTagsMustache)},
	id.ErrorTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Error HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentErrorMustache)},
	id.BaseCSSZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeySyntax:     "css",
			meta.KeyNoIndex:    meta.ValueTrue,
		},
		domain.NewContent(contentBaseCSS)},
	id.EmojiZid: {
		constHeader{
			meta.KeyTitle:      "Generic Emoji",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeySyntax:     meta.ValueSyntaxGif,
			meta.KeyReadOnly:   meta.ValueTrue,
		},
		domain.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {
		constHeader{
			meta.KeyTitle:  "New Menu",
			meta.KeyRole:   meta.ValueRoleConfiguration,
			meta.KeySyntax: meta.ValueSyntaxZmk,
			meta.KeyLang:   meta.ValueLangEN,
		},
		domain.NewContent(contentNewTOCZettel)},
	id.TemplateNewZettelZid: {
		constHeader{
			meta.KeyTitle:  "New Zettel",
			meta.KeyRole:   meta.ValueRoleZettel,
			meta.KeySyntax: meta.ValueSyntaxZmk,
		},
		""},
	id.TemplateNewUserZid: {
		constHeader{
			meta.KeyTitle:      "New User",
			meta.KeyRole:       meta.ValueRoleUser,
			meta.KeyCredential: "",
			meta.KeyUserID:     "",
			meta.KeyUserRole:   meta.ValueUserRoleReader,
			meta.KeySyntax:     meta.ValueSyntaxNone,
		},
		""},
	id.DefaultHomeZid: {
		constHeader{
			meta.KeyTitle:  "Home",
			meta.KeyRole:   meta.ValueRoleZettel,
			meta.KeySyntax: meta.ValueSyntaxZmk,
			meta.KeyLang:   meta.ValueLangEN,
		},
		domain.NewContent(contentHomeZettel)},
}

//go:embed license.txt
var contentLicense string

//go:embed contributors.zettel
var contentContributors string

//go:embed dependencies.zettel
var contentDependencies string

//go:embed base.mustache
var contentBaseMustache string

//go:embed login.mustache
var contentLoginMustache string

//go:embed zettel.mustache
var contentZettelMustache string

//go:embed info.mustache
var contentInfoMustache string

//go:embed context.mustache
var contentContextMustache string

//go:embed form.mustache
var contentFormMustache string

//go:embed rename.mustache
var contentRenameMustache string

//go:embed delete.mustache
var contentDeleteMustache string

//go:embed listzettel.mustache
var contentListZettelMustache string

//go:embed listroles.mustache
var contentListRolesMustache string

//go:embed listtags.mustache
var contentListTagsMustache string

//go:embed error.mustache
var contentErrorMustache string

//go:embed base.css
var contentBaseCSS string

//go:embed emoji_spin.gif
var contentEmoji string

//go:embed newtoc.zettel
var contentNewTOCZettel string

//go:embed home.zettel
var contentHomeZettel string

Deleted place/constplace/context.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<nav>
<header>
<h1>{{Title}}</h1>
<div class="zs-meta">
<a href="{{{InfoURL}}}">Info</a>
&#183; <a href="?dir=backward">Backward</a>
&#183; <a href="?dir=both">Both</a>
&#183; <a href="?dir=forward">Forward</a>
&#183; Depth:{{#Depths}}&#x2000;<a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}}
</div>
</header>
<p><a href="{{{Start.URL}}}">{{{Start.Text}}}</a></p>
<ul>
{{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li>
{{/Metas}}</ul>
</nav>

Deleted place/constplace/contributors.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
Zettelstore is a software for humans made from humans.

=== Licensor(s)
* Detlef Stern [[mailto:ds@zettelstore.de]]
** Main author
** Maintainer

=== Contributors

Deleted place/constplace/delete.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<article>
<header>
<h1>Delete Zettel {{Zid}}</h1>
</header>
<p>Do you really want to delete this zettel?</p>
<dl>
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
<form method="POST">
<input class="zs-button" type="submit" value="Delete">
</form>
</article>
{{end}}

Deleted place/constplace/dependencies.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





















































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Zettelstore is made with the help of other software and other artifacts.
Thank you very much!

This zettel lists all of them, together with their license.

=== Go runtime and associated libraries
; License
: BSD 3-Clause "New" or "Revised" License
```
Copyright (c) 2009 The Go Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

=== Fsnotify
; URL
: [[https://fsnotify.org/]]
; License
: BSD 3-Clause "New" or "Revised" License
; Source
: [[https://github.com/fsnotify/fsnotify]]
```
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2012-2019 fsnotify Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

=== 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

Copyright (c) 2019 Yusuke Inuzuka

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 place/constplace/emoji_spin.gif.

cannot compute difference between binary files

Deleted place/constplace/error.mustache.

1
2
3
4
5
6






-
-
-
-
-
-
<article>
<header>
<h1>{{ErrorTitle}}</h1>
</header>
{{ErrorText}}
</article>

Deleted place/constplace/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






































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<article>
<header>
<h1>{{Heading}}</h1>
</header>
<form method="POST">
<div>
<label for="title">Title</label>
<input class="zs-input" type="text" id="title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus>
</div>
<div>
<div>
<label for="role">Role</label>
<input class="zs-input" type="text" id="role" name="role" placeholder="role.." value="{{MetaRole}}">
</div>
<label for="tags">Tags</label>
<input class="zs-input" type="text" id="tags" name="tags" placeholder="#tag" value="{{MetaTags}}">
</div>
<div>
<label for="meta">Metadata</label>
<textarea class="zs-input" id="meta" name="meta" rows="4" placeholder="metakey: metavalue">
{{#MetaPairsRest}}
{{Key}}: {{Value}}
{{/MetaPairsRest}}
</textarea>
</div>
<div>
<label for="syntax">Syntax</label>
<input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}">
</div>
<div>
{{#IsTextContent}}
<label for="content">Content</label>
<textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea>
{{/IsTextContent}}
</div>
<input class="zs-button" type="submit" value="Submit">
</form>
</article>

Deleted place/constplace/home.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












































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
=== 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.
To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore.

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]]
* [[Zettelstore Operating System|00000000000003]]
* [[Zettelstore Startup Configuration|00000000000096]]
* [[Zettelstore Runtime Configuration|00000000000100]]

Additionally, you have to describe, what you have done before that error occurs
and what you have expected instead.
Please do not forget to include the error message, if there is one.

Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"".
Otherwise, only some zettel are linked.
To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]:
please set the metadata value of the key ''expert-mode'' to true.
To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata.

=== Information about this zettel
This zettel is your home zettel.
It is part of the Zettelstore software itself.
Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel.

You can change the content of this zettel by clicking on ""Edit"" above.
This allows you to customize your home zettel.

Alternatively, you can designate another zettel as your home zettel.
Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''.
Its value is the identifier of the zettel that should act as the new home zettel.
You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above.
The identifier of this zettel is ''00010000000000''.
If you provide a wrong identifier, this zettel will be shown as the home zettel.
Take a look inside the manual for further details.

Deleted place/constplace/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












































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<article>
<header>
<h1>Information for Zettel {{Zid}}</h1>
<a href="{{{WebURL}}}">Web</a>
&#183; <a href="{{{ContextURL}}}">Context</a>
{{#CanWrite}} &#183; <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}}
{{#CanFolge}} &#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#CanCopy}} &#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanRename}}&#183; <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}}
{{#CanDelete}}&#183; <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}}
</header>
<h2>Interpreted Metadata</h2>
<table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table>
{{#HasLinks}}
<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}}
{{#HasExtLinks}}
<h3>External</h3>
<ul>
{{#ExtLinks}}
<li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li>
{{/ExtLinks}}
</ul>
{{/HasExtLinks}}
{{/HasLinks}}
<h2>Parts and format</h3>
<table>
{{#Matrix}}
<tr>
{{#Elements}}{{#HasURL}}<td><a href="{{{URL}}}">{{Text}}</td>{{/HasURL}}{{^HasURL}}<th>{{Text}}</th>{{/HasURL}}
{{/Elements}}
</tr>
{{/Matrix}}
</table>
{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}}
</article>

Deleted place/constplace/license.txt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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







































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Copyright (c) 2020-2021 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
translations of the other languages.


-------------------------------------------------------------------------------


EUROPEAN UNION PUBLIC LICENCE v. 1.2
EUPL © the European Union 2007, 2016

This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
below) which is provided under the terms of this Licence. Any use of the Work,
other than as authorised under this Licence is prohibited (to the extent such
use is covered by a right of the copyright holder of the Work).

The Work is provided under the terms of this Licence when the Licensor (as
defined below) has placed the following notice immediately following the
copyright notice for the Work:

                          Licensed under the EUPL

or has expressed by any other means his willingness to license under the EUPL.

1. Definitions

In this Licence, the following terms have the following meaning:

— ‘The Licence’: this Licence.
— ‘The Original Work’: the work or software distributed or communicated by the
  Licensor under this Licence, available as Source Code and also as Executable
  Code as the case may be.
— ‘Derivative Works’: the works or software that could be created by the
  Licensee, based upon the Original Work or modifications thereof. This Licence
  does not define the extent of modification or dependence on the Original Work
  required in order to classify a work as a Derivative Work; this extent is
  determined by copyright law applicable in the country mentioned in Article
  15.
— ‘The Work’: the Original Work or its Derivative Works.
— ‘The Source Code’: the human-readable form of the Work which is the most
  convenient for people to study and modify.
— ‘The Executable Code’: any code which has generally been compiled and which
  is meant to be interpreted by a computer as a program.
— ‘The Licensor’: the natural or legal person that distributes or communicates
  the Work under the Licence.
— ‘Contributor(s)’: any natural or legal person who modifies the Work under the
  Licence, or otherwise contributes to the creation of a Derivative Work.
— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
  the Work under the terms of the Licence.
— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
  renting, distributing, communicating, transmitting, or otherwise making
  available, online or offline, copies of the Work or providing access to its
  essential functionalities at the disposal of any other natural or legal
  person.

2. Scope of the rights granted by the Licence

The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
sublicensable licence to do the following, for the duration of copyright vested
in the Original Work:

— use the Work in any circumstance and for all usage,
— reproduce the Work,
— modify the Work, and make Derivative Works based upon the Work,
— communicate to the public, including the right to make available or display
  the Work or copies thereof to the public and perform publicly, as the case
  may be, the Work,
— distribute the Work or copies thereof,
— lend and rent the Work or copies thereof,
— sublicense rights in the Work or copies thereof.

Those rights can be exercised on any media, supports and formats, whether now
known or later invented, as far as the applicable law permits so.

In the countries where moral rights apply, the Licensor waives his right to
exercise his moral right to the extent allowed by law in order to make
effective the licence of the economic rights here above listed.

The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
any patents held by the Licensor, to the extent necessary to make use of the
rights granted on the Work under this Licence.

3. Communication of the Source Code

The Licensor may provide the Work either in its Source Code form, or as
Executable Code. If the Work is provided as Executable Code, the Licensor
provides in addition a machine-readable copy of the Source Code of the Work
along with each copy of the Work that the Licensor distributes or indicates, in
a notice following the copyright notice attached to the Work, a repository
where the Source Code is easily and freely accessible for as long as the
Licensor continues to distribute or communicate the Work.

4. Limitations on copyright

Nothing in this Licence is intended to deprive the Licensee of the benefits
from any exception or limitation to the exclusive rights of the rights owners
in the Work, of the exhaustion of those rights or of other applicable
limitations thereto.

5. Obligations of the Licensee

The grant of the rights mentioned above is subject to some restrictions and
obligations imposed on the Licensee. Those obligations are the following:

Attribution right: The Licensee shall keep intact all copyright, patent or
trademarks notices and all notices that refer to the Licence and to the
disclaimer of warranties. The Licensee must include a copy of such notices and
a copy of the Licence with every copy of the Work he/she distributes or
communicates. The Licensee must cause any Derivative Work to carry prominent
notices stating that the Work has been modified and the date of modification.

Copyleft clause: If the Licensee distributes or communicates copies of the
Original Works or Derivative Works, this Distribution or Communication will be
done under the terms of this Licence or of a later version of this Licence
unless the Original Work is expressly distributed only under this version of
the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
(becoming Licensor) cannot offer or impose any additional terms or conditions
on the Work or Derivative Work that alter or restrict the terms of the Licence.

Compatibility clause: If the Licensee Distributes or Communicates Derivative
Works or copies thereof based upon both the Work and another work licensed
under a Compatible Licence, this Distribution or Communication can be done
under the terms of this Compatible Licence. For the sake of this clause,
‘Compatible Licence’ refers to the licences listed in the appendix attached to
this Licence. Should the Licensee's obligations under the Compatible Licence
conflict with his/her obligations under this Licence, the obligations of the
Compatible Licence shall prevail.

Provision of Source Code: When distributing or communicating copies of the
Work, the Licensee will provide a machine-readable copy of the Source Code or
indicate a repository where this Source will be easily and freely available for
as long as the Licensee continues to distribute or communicate the Work.

Legal Protection: This Licence does not grant permission to use the trade
names, trademarks, service marks, or names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the copyright notice.

6. Chain of Authorship

The original Licensor warrants that the copyright in the Original Work granted
hereunder is owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.

Each Contributor warrants that the copyright in the modifications he/she brings
to the Work are owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.

Each time You accept the Licence, the original Licensor and subsequent
Contributors grant You a licence to their contributions to the Work, under the
terms of this Licence.

7. Disclaimer of Warranty

The Work is a work in progress, which is continuously improved by numerous
Contributors. It is not a finished work and may therefore contain defects or
‘bugs’ inherent to this type of development.

For the above reason, the Work is provided under the Licence on an ‘as is’
basis and without warranties of any kind concerning the Work, including without
limitation merchantability, fitness for a particular purpose, absence of
defects or errors, accuracy, non-infringement of intellectual property rights
other than copyright as stated in Article 6 of this Licence.

This disclaimer of warranty is an essential part of the Licence and a condition
for the grant of any rights to the Work.

8. Disclaimer of Liability

Except in the cases of wilful misconduct or damages directly caused to natural
persons, the Licensor will in no event be liable for any direct or indirect,
material or moral, damages of any kind, arising out of the Licence or of the
use of the Work, including without limitation, damages for loss of goodwill,
work stoppage, computer failure or malfunction, loss of data or any commercial
damage, even if the Licensor has been advised of the possibility of such
damage. However, the Licensor will be liable under statutory product liability
laws as far such laws apply to the Work.

9. Additional agreements

While distributing the Work, You may choose to conclude an additional
agreement, defining obligations or services consistent with this Licence.
However, if accepting obligations, You may act only on your own behalf and on
your sole responsibility, not on behalf of the original Licensor or any other
Contributor, and only if You agree to indemnify, defend, and hold each
Contributor harmless for any liability incurred by, or claims asserted against
such Contributor by the fact You have accepted any warranty or additional
liability.

10. Acceptance of the Licence

The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
placed under the bottom of a window displaying the text of this Licence or by
affirming consent in any other similar way, in accordance with the rules of
applicable law. Clicking on that icon indicates your clear and irrevocable
acceptance of this Licence and all of its terms and conditions.

Similarly, you irrevocably accept this Licence and all of its terms and
conditions by exercising any rights granted to You by Article 2 of this
Licence, such as the use of the Work, the creation by You of a Derivative Work
or the Distribution or Communication by You of the Work or copies thereof.

11. Information to the public

In case of any Distribution or Communication of the Work by means of electronic
communication by You (for example, by offering to download the Work from
a remote location) the distribution channel or media (for example, a website)
must at least provide to the public the information requested by the applicable
law regarding the Licensor, the Licence and the way it may be accessible,
concluded, stored and reproduced by the Licensee.

12. Termination of the Licence

The Licence and the rights granted hereunder will terminate automatically upon
any breach by the Licensee of the terms of the Licence.

Such a termination will not terminate the licences of any person who has
received the Work from the Licensee under the Licence, provided such persons
remain in full compliance with the Licence.

13. Miscellaneous

Without prejudice of Article 9 above, the Licence represents the complete
agreement between the Parties as to the Work.

If any provision of the Licence is invalid or unenforceable under applicable
law, this will not affect the validity or enforceability of the Licence as
a whole. Such provision will be construed or reformed so as necessary to make
it valid and enforceable.

The European Commission may publish other linguistic versions or new versions
of this Licence or updated versions of the Appendix, so far this is required
and reasonable, without reducing the scope of the rights granted by the
Licence. New versions of the Licence will be published with a unique version
number.

All linguistic versions of this Licence, approved by the European Commission,
have identical value. Parties can take advantage of the linguistic version of
their choice.

14. Jurisdiction

Without prejudice to specific agreement between parties,

— any litigation resulting from the interpretation of this License, arising
  between the European Union institutions, bodies, offices or agencies, as
  a Licensor, and any Licensee, will be subject to the jurisdiction of the
  Court of Justice of the European Union, as laid down in article 272 of the
  Treaty on the Functioning of the European Union,
— any litigation arising between other parties and resulting from the
  interpretation of this License, will be subject to the exclusive jurisdiction
  of the competent court where the Licensor resides or conducts its primary
  business.

15. Applicable Law

Without prejudice to specific agreement between parties,

— this Licence shall be governed by the law of the European Union Member State
  where the Licensor has his seat, resides or has his registered office,
— this licence shall be governed by Belgian law if the Licensor has no seat,
  residence or registered office inside a European Union Member State.


                                  Appendix


‘Compatible Licences’ according to Article 5 EUPL are:

— GNU General Public License (GPL) v. 2, v. 3
— GNU Affero General Public License (AGPL) v. 3
— Open Software License (OSL) v. 2.1, v. 3.0
— Eclipse Public License (EPL) v. 1.0
— CeCILL v. 2.0, v. 2.1
— Mozilla Public Licence (MPL) v. 2
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
  works other than software
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
  Reciprocity (LiLiQ-R+)

The European Commission may update this Appendix to later versions of the above
licences without producing a new version of the EUPL, as long as they provide
the rights granted in Article 2 of this Licence and protect the covered Source
Code from exclusive appropriation.

All other changes or additions to this Appendix require the production of a new
EUPL version.

Deleted place/constplace/listroles.mustache.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
<nav>
<header>
<h1>Currently used roles</h1>
</header>
<ul>
{{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Roles}}</ul>
</nav>

Deleted place/constplace/listtags.mustache.

1
2
3
4
5
6
7
8
9
10










-
-
-
-
-
-
-
-
-
-
<nav>
<header>
<h1>Currently used tags</h1>
<div class="zs-meta">
<a href="{{{ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}}
</div>
</header>
{{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup>
{{/Tags}}
</nav>

Deleted place/constplace/listzettel.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19



















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<nav>
<header>
<h1>{{Title}}</h1>
</header>
<ul>
{{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li>
{{/Metas}}</ul>
{{#HasPrevNext}}
<p>
{{#HasPrev}}
<a href="{{{PrevURL}}}" rel="prev">Prev</a>
{{#HasNext}},{{/HasNext}}
{{/HasPrev}}
{{#HasNext}}
<a href="{{{NextURL}}}" rel="next">Next</a>
{{/HasNext}}
</p>
{{/HasPrevNext}}
</nav>

Deleted place/constplace/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="?_format=html">
<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>
<input class="zs-button" type="submit" value="Login">
</form>
</article>

Deleted place/constplace/newtoc.zettel.

1
2
3
4




-
-
-
-
This zettel lists all zettel that should act as a template for new zettel.
These zettel will be included in the ""New"" menu of the WebUI.
* [[New Zettel|00000000090001]]
* [[New User|00000000090002]]

Deleted place/constplace/rename.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19



















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<article>
<header>
<h1>Rename Zettel {{.Zid}}</h1>
</header>
<p>Do you really want to rename this zettel?</p>
<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}}">
<input class="zs-button" type="submit" value="Rename">
</form>
<dl>
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
</article>

Deleted place/constplace/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




























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<article>
<header>
<h1>{{{HTMLTitle}}}</h1>
<div class="zs-meta">
{{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> &#183;{{/CanWrite}}
{{Zid}} &#183;
<a href="{{{InfoURL}}}">Info</a> &#183;
(<a href="{{{RoleURL}}}">{{RoleText}}</a>)
{{#HasTags}}&#183; {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}}
{{#CanCopy}}&#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanFolge}}&#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#FolgeRefs}}<br>Folge: {{{FolgeRefs}}}{{/FolgeRefs}}
{{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}}
{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}}
</div>
</header>
{{{Content}}}
{{#HasBackLinks}}
<details>
<summary>Additional links to this zettel</summary>
<ul>
{{#BackLinks}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/BackLinks}}
</ul>
</details>
{{/HasBackLinks}}
</article>

Deleted place/dirplace/directory/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





















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package directory manages the directory interface of a dirstore.
package directory

import "zettelstore.de/z/domain/id"

// Service is the interface of a directory service.
type Service interface {
	Start() error
	Stop() error
	NumEntries() (int, error)
	GetEntries() ([]*Entry, error)
	GetEntry(zid id.Zid) (*Entry, error)
	GetNew() (*Entry, error)
	UpdateEntry(entry *Entry) error
	RenameEntry(curEntry, newEntry *Entry) error
	DeleteEntry(zid id.Zid) error
}

// MetaSpec defines all possibilities where meta data can be stored.
type MetaSpec int

// Constants for MetaSpec
const (
	_              MetaSpec = iota
	MetaSpecNone            // no meta information
	MetaSpecFile            // meta information is in meta file
	MetaSpecHeader          // meta information is in header
)

// Entry stores everything for a directory entry.
type Entry struct {
	Zid         id.Zid
	MetaSpec    MetaSpec // location of meta information
	MetaPath    string   // file path of meta information
	ContentPath string   // file path of zettel content
	ContentExt  string   // (normalized) file extension of zettel content
	Duplicates  bool     // multiple content files
}

// IsValid checks whether the entry is valid.
func (e *Entry) IsValid() bool {
	return e != nil && e.Zid.IsValid()
}

Deleted place/dirplace/dirplace.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

























































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package dirplace provides a directory-based zettel place.
package dirplace

import (
	"context"
	"errors"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/dirplace/directory"
	"zettelstore.de/z/place/fileplace"
	"zettelstore.de/z/place/manager"
	"zettelstore.de/z/search"
)

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) {
		path := getDirPath(u)
		if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
			return nil, err
		}
		dirSrvSpec, defWorker, maxWorker := getDirSrvInfo(u.Query().Get("type"))
		dp := dirPlace{
			location:   u.String(),
			readonly:   getQueryBool(u, "readonly"),
			cdata:      *cdata,
			dir:        path,
			dirRescan:  time.Duration(getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second,
			dirSrvSpec: dirSrvSpec,
			fSrvs:      uint32(getQueryInt(u, "worker", 1, defWorker, maxWorker)),
		}
		return &dp, nil
	})
}

type directoryServiceSpec int

const (
	_ directoryServiceSpec = iota
	dirSrvAny
	dirSrvSimple
	dirSrvNotify
)

func getDirPath(u *url.URL) string {
	if u.Opaque != "" {
		return filepath.Clean(u.Opaque)
	}
	return filepath.Clean(u.Path)
}

func getQueryBool(u *url.URL, key string) bool {
	_, ok := u.Query()[key]
	return ok
}

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
}

// dirPlace uses a directory to store zettel as files.
type dirPlace struct {
	location   string
	readonly   bool
	cdata      manager.ConnectData
	dir        string
	dirRescan  time.Duration
	dirSrvSpec directoryServiceSpec
	dirSrv     directory.Service
	mustNotify bool
	fSrvs      uint32
	fCmds      []chan fileCmd
	mxCmds     sync.RWMutex
}

func (dp *dirPlace) Location() string {
	return dp.location
}

func (dp *dirPlace) Start(ctx context.Context) error {
	dp.mxCmds.Lock()
	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
	for i := uint32(0); i < dp.fSrvs; i++ {
		cc := make(chan fileCmd)
		go fileService(i, cc)
		dp.fCmds = append(dp.fCmds, cc)
	}
	dp.setupDirService()
	dp.mxCmds.Unlock()
	if dp.dirSrv == nil {
		panic("No directory service")
	}
	return dp.dirSrv.Start()
}

func (dp *dirPlace) Stop(ctx context.Context) error {
	dirSrv := dp.dirSrv
	dp.dirSrv = nil
	err := dirSrv.Stop()
	for _, c := range dp.fCmds {
		close(c)
	}
	return err
}

func (dp *dirPlace) notifyChanged(reason place.UpdateReason, zid id.Zid) {
	if dp.mustNotify {
		if chci := dp.cdata.Notify; chci != nil {
			chci <- place.UpdateInfo{Reason: reason, Zid: zid}
		}
	}
}

func (dp *dirPlace) getFileChan(zid id.Zid) chan fileCmd {
	// Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
	var sum uint32 = 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 *dirPlace) CanCreateZettel(ctx context.Context) bool {
	return !dp.readonly
}

func (dp *dirPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	if dp.readonly {
		return id.Invalid, place.ErrReadOnly
	}

	entry, err := dp.dirSrv.GetNew()
	if err != nil {
		return id.Invalid, err
	}
	meta := zettel.Meta
	meta.Zid = entry.Zid
	dp.updateEntryFromMeta(entry, meta)

	err = setZettel(dp, entry, zettel)
	if err == nil {
		dp.dirSrv.UpdateEntry(entry)
	}
	dp.notifyChanged(place.OnUpdate, meta.Zid)
	return meta.Zid, err
}

func (dp *dirPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	entry, err := dp.dirSrv.GetEntry(zid)
	if err != nil || !entry.IsValid() {
		return domain.Zettel{}, place.ErrNotFound
	}
	m, c, err := getMetaContent(dp, entry, zid)
	if err != nil {
		return domain.Zettel{}, err
	}
	dp.cleanupMeta(ctx, m)
	zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)}
	return zettel, nil
}

func (dp *dirPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	entry, err := dp.dirSrv.GetEntry(zid)
	if err != nil || !entry.IsValid() {
		return nil, place.ErrNotFound
	}
	m, err := getMeta(dp, entry, zid)
	if err != nil {
		return nil, err
	}
	dp.cleanupMeta(ctx, m)
	return m, nil
}

func (dp *dirPlace) FetchZids(ctx context.Context) (id.Set, error) {
	entries, err := dp.dirSrv.GetEntries()
	if err != nil {
		return nil, err
	}
	result := id.NewSetCap(len(entries))
	for _, entry := range entries {
		result[entry.Zid] = true
	}
	return result, nil
}

func (dp *dirPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
	entries, err := dp.dirSrv.GetEntries()
	if err != nil {
		return nil, err
	}
	res = make([]*meta.Meta, 0, len(entries))
	// The following loop could be parallelized if needed for performance.
	for _, entry := range entries {
		m, err1 := getMeta(dp, entry, entry.Zid)
		err = err1
		if err != nil {
			continue
		}
		dp.cleanupMeta(ctx, m)
		dp.cdata.Enricher.Enrich(ctx, m)

		if match(m) {
			res = append(res, m)
		}
	}
	if err != nil {
		return nil, err
	}
	return res, nil
}

func (dp *dirPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	return !dp.readonly
}

func (dp *dirPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	if dp.readonly {
		return place.ErrReadOnly
	}

	meta := zettel.Meta
	if !meta.Zid.IsValid() {
		return &place.ErrInvalidID{Zid: meta.Zid}
	}
	entry, err := dp.dirSrv.GetEntry(meta.Zid)
	if err != nil {
		return err
	}
	if !entry.IsValid() {
		// Existing zettel, but new in this place.
		entry = &directory.Entry{Zid: meta.Zid}
		dp.updateEntryFromMeta(entry, meta)
	} else if entry.MetaSpec == directory.MetaSpecNone {
		defaultMeta := fileplace.CalcDefaultMeta(entry.Zid, entry.ContentExt)
		if !meta.Equal(defaultMeta, true) {
			dp.updateEntryFromMeta(entry, meta)
			dp.dirSrv.UpdateEntry(entry)
		}
	}
	err = setZettel(dp, entry, zettel)
	if err == nil {
		dp.notifyChanged(place.OnUpdate, meta.Zid)
	}
	return err
}

func (dp *dirPlace) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) {
	entry.MetaSpec, entry.ContentExt = dp.calcSpecExt(meta)
	basePath := filepath.Join(dp.dir, entry.Zid.String())
	if entry.MetaSpec == directory.MetaSpecFile {
		entry.MetaPath = basePath + ".meta"
	}
	entry.ContentPath = basePath + "." + entry.ContentExt
	entry.Duplicates = false
}

func (dp *dirPlace) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) {
	if m.YamlSep {
		return directory.MetaSpecHeader, "zettel"
	}
	syntax := m.GetDefault(meta.KeySyntax, "bin")
	switch syntax {
	case meta.ValueSyntaxNone, meta.ValueSyntaxZmk:
		return directory.MetaSpecHeader, "zettel"
	}
	for _, s := range dp.cdata.Config.GetZettelFileSyntax() {
		if s == syntax {
			return directory.MetaSpecHeader, "zettel"
		}
	}
	return directory.MetaSpecFile, syntax
}

func (dp *dirPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	return !dp.readonly
}

func (dp *dirPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if dp.readonly {
		return place.ErrReadOnly
	}
	if curZid == newZid {
		return nil
	}
	curEntry, err := dp.dirSrv.GetEntry(curZid)
	if err != nil || !curEntry.IsValid() {
		return place.ErrNotFound
	}

	// Check whether zettel with new ID already exists in this place
	if _, err = dp.GetMeta(ctx, newZid); err == nil {
		return &place.ErrInvalidID{Zid: newZid}
	}

	oldMeta, oldContent, err := getMetaContent(dp, curEntry, curZid)
	if err != nil {
		return err
	}

	newEntry := directory.Entry{
		Zid:         newZid,
		MetaSpec:    curEntry.MetaSpec,
		MetaPath:    renamePath(curEntry.MetaPath, curZid, newZid),
		ContentPath: renamePath(curEntry.ContentPath, curZid, newZid),
		ContentExt:  curEntry.ContentExt,
	}

	if err = dp.dirSrv.RenameEntry(curEntry, &newEntry); err != nil {
		return err
	}
	oldMeta.Zid = newZid
	newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)}
	if err = setZettel(dp, &newEntry, newZettel); err != nil {
		// "Rollback" rename. No error checking...
		dp.dirSrv.RenameEntry(&newEntry, curEntry)
		return err
	}
	err = deleteZettel(dp, curEntry, curZid)
	if err == nil {
		dp.notifyChanged(place.OnDelete, curZid)
		dp.notifyChanged(place.OnUpdate, newZid)
	}
	return err
}

func (dp *dirPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	if dp.readonly {
		return false
	}
	entry, err := dp.dirSrv.GetEntry(zid)
	return err == nil && entry.IsValid()
}

func (dp *dirPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if dp.readonly {
		return place.ErrReadOnly
	}

	entry, err := dp.dirSrv.GetEntry(zid)
	if err != nil || !entry.IsValid() {
		return nil
	}
	dp.dirSrv.DeleteEntry(zid)
	err = deleteZettel(dp, entry, zid)
	if err == nil {
		dp.notifyChanged(place.OnDelete, zid)
	}
	return err
}

func (dp *dirPlace) ReadStats(st *place.ManagedPlaceStats) {
	st.ReadOnly = dp.readonly
	st.Zettel, _ = dp.dirSrv.NumEntries()
}

func (dp *dirPlace) cleanupMeta(ctx context.Context, m *meta.Meta) {
	if role, ok := m.Get(meta.KeyRole); !ok || role == "" {
		m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole())
	}
	if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax())
	}
}

func renamePath(path string, curID, newID id.Zid) string {
	dir, file := filepath.Split(path)
	if cur := curID.String(); strings.HasPrefix(file, cur) {
		file = newID.String() + file[len(cur):]
		return filepath.Join(dir, file)
	}
	return path
}

Deleted place/dirplace/makedir.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package dirplace provides a directory-based zettel place.
package dirplace

import (
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place/dirplace/notifydir"
	"zettelstore.de/z/place/dirplace/simpledir"
)

func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) {
	for count := 0; count < 2; count++ {
		switch dirType {
		case kernel.PlaceDirTypeNotify:
			return dirSrvNotify, 7, 1499
		case kernel.PlaceDirTypeSimple:
			return dirSrvSimple, 1, 1
		default:
			dirType = kernel.Main.GetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType).(string)
		}
	}
	panic("unable to set default dir place type: " + dirType)
}

func (dp *dirPlace) setupDirService() {
	switch dp.dirSrvSpec {
	case dirSrvSimple:
		dp.dirSrv = simpledir.NewService(dp.dir)
		dp.mustNotify = true
	default:
		dp.dirSrv = notifydir.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify)
		dp.mustNotify = false
	}
}

Deleted place/dirplace/notifydir/notifydir.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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






























































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package notifydir manages the notified directory part of a dirstore.
package notifydir

import (
	"time"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/dirplace/directory"
)

// notifyService specifies a directory scan service.
type notifyService struct {
	dirPath    string
	rescanTime time.Duration
	done       chan struct{}
	cmds       chan dirCmd
	infos      chan<- place.UpdateInfo
}

// NewService creates a new directory service.
func NewService(directoryPath string, rescanTime time.Duration, chci chan<- place.UpdateInfo) directory.Service {
	srv := &notifyService{
		dirPath:    directoryPath,
		rescanTime: rescanTime,
		cmds:       make(chan dirCmd),
		infos:      chci,
	}
	return srv
}

// Start makes the directory service operational.
func (srv *notifyService) Start() error {
	tick := make(chan struct{})
	rawEvents := make(chan *fileEvent)
	events := make(chan *fileEvent)

	ready := make(chan int)
	go srv.directoryService(events, ready)
	go collectEvents(events, rawEvents)
	go watchDirectory(srv.dirPath, rawEvents, tick)

	if srv.done != nil {
		panic("src.done already set")
	}
	srv.done = make(chan struct{})
	go ping(tick, srv.rescanTime, srv.done)
	<-ready
	return nil
}

// Stop stops the directory service.
func (srv *notifyService) Stop() error {
	close(srv.done)
	srv.done = nil
	return nil
}

func (srv *notifyService) notifyChange(reason place.UpdateReason, zid id.Zid) {
	if chci := srv.infos; chci != nil {
		chci <- place.UpdateInfo{Reason: reason, Zid: zid}
	}
}

// NumEntries returns the number of managed zettel.
func (srv *notifyService) NumEntries() (int, error) {
	resChan := make(chan resNumEntries)
	srv.cmds <- &cmdNumEntries{resChan}
	return <-resChan, nil
}

// GetEntries returns an unsorted list of all current directory entries.
func (srv *notifyService) GetEntries() ([]*directory.Entry, error) {
	resChan := make(chan resGetEntries)
	srv.cmds <- &cmdGetEntries{resChan}
	return <-resChan, nil
}

// GetEntry returns the entry with the specified zettel id. If there is no such
// zettel id, an empty entry is returned.
func (srv *notifyService) GetEntry(zid id.Zid) (*directory.Entry, error) {
	resChan := make(chan resGetEntry)
	srv.cmds <- &cmdGetEntry{zid, resChan}
	return <-resChan, nil
}

// GetNew returns an entry with a new zettel id.
func (srv *notifyService) GetNew() (*directory.Entry, error) {
	resChan := make(chan resNewEntry)
	srv.cmds <- &cmdNewEntry{resChan}
	result := <-resChan
	return result.entry, result.err
}

// UpdateEntry notifies the directory of an updated entry.
func (srv *notifyService) UpdateEntry(entry *directory.Entry) error {
	resChan := make(chan struct{})
	srv.cmds <- &cmdUpdateEntry{entry, resChan}
	<-resChan
	return nil
}

// RenameEntry notifies the directory of an renamed entry.
func (srv *notifyService) RenameEntry(curEntry, newEntry *directory.Entry) error {
	resChan := make(chan resRenameEntry)
	srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan}
	return <-resChan
}

// DeleteEntry removes a zettel id from the directory of entries.
func (srv *notifyService) DeleteEntry(zid id.Zid) error {
	resChan := make(chan struct{})
	srv.cmds <- &cmdDeleteEntry{zid, resChan}
	<-resChan
	return nil
}

Deleted place/dirplace/notifydir/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
55
56
57
58
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































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package notifydir manages the notified directory part of a dirstore.
package notifydir

import (
	"log"
	"time"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/dirplace/directory"
)

// ping sends every tick a signal to reload the directory list
func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) {
	ticker := time.NewTicker(rescanTime)
	defer close(tick)
	for {
		select {
		case _, ok := <-ticker.C:
			if !ok {
				return
			}
			tick <- struct{}{}
		case _, ok := <-done:
			if !ok {
				ticker.Stop()
				return
			}
		}
	}
}

func newEntry(ev *fileEvent) *directory.Entry {
	de := new(directory.Entry)
	de.Zid = ev.zid
	updateEntry(de, ev)
	return de
}

func updateEntry(de *directory.Entry, ev *fileEvent) {
	if ev.ext == "meta" {
		de.MetaSpec = directory.MetaSpecFile
		de.MetaPath = ev.path
		return
	}
	if de.ContentExt != "" && de.ContentExt != ev.ext {
		de.Duplicates = true
		return
	}
	if de.MetaSpec != directory.MetaSpecFile {
		if ev.ext == "zettel" {
			de.MetaSpec = directory.MetaSpecHeader
		} else {
			de.MetaSpec = directory.MetaSpecNone
		}
	}
	de.ContentPath = ev.path
	de.ContentExt = ev.ext
}

type dirMap map[id.Zid]*directory.Entry

func dirMapUpdate(dm dirMap, ev *fileEvent) {
	de := dm[ev.zid]
	if de == nil {
		dm[ev.zid] = newEntry(ev)
		return
	}
	updateEntry(de, ev)
}

func deleteFromMap(dm dirMap, ev *fileEvent) {
	if ev.ext == "meta" {
		if entry, ok := dm[ev.zid]; ok {
			if entry.MetaSpec == directory.MetaSpecFile {
				entry.MetaSpec = directory.MetaSpecNone
				return
			}
		}
	}
	delete(dm, ev.zid)
}

// directoryService is the main service.
func (srv *notifyService) directoryService(events <-chan *fileEvent, ready chan<- int) {
	curMap := make(dirMap)
	var newMap dirMap
	for {
		select {
		case ev, ok := <-events:
			if !ok {
				return
			}
			switch ev.status {
			case fileStatusReloadStart:
				newMap = make(dirMap)
			case fileStatusReloadEnd:
				curMap = newMap
				newMap = nil
				if ready != nil {
					ready <- len(curMap)
					close(ready)
					ready = nil
				}
				srv.notifyChange(place.OnReload, id.Invalid)
			case fileStatusError:
				log.Println("DIRPLACE", "ERROR", ev.err)
			case fileStatusUpdate:
				srv.processFileUpdateEvent(ev, curMap, newMap)
			case fileStatusDelete:
				srv.processFileDeleteEvent(ev, curMap, newMap)
			}
		case cmd, ok := <-srv.cmds:
			if ok {
				cmd.run(curMap)
			}
		}
	}
}

func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) {
	if newMap != nil {
		dirMapUpdate(newMap, ev)
	} else {
		dirMapUpdate(curMap, ev)
		srv.notifyChange(place.OnUpdate, ev.zid)
	}
}

func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) {
	if newMap != nil {
		deleteFromMap(newMap, ev)
	} else {
		deleteFromMap(curMap, ev)
		srv.notifyChange(place.OnDelete, ev.zid)
	}
}

type dirCmd interface {
	run(m dirMap)
}

type cmdNumEntries struct {
	result chan<- resNumEntries
}
type resNumEntries = int

func (cmd *cmdNumEntries) run(m dirMap) {
	cmd.result <- len(m)
}

type cmdGetEntries struct {
	result chan<- resGetEntries
}
type resGetEntries []*directory.Entry

func (cmd *cmdGetEntries) run(m dirMap) {
	res := make([]*directory.Entry, len(m))
	i := 0
	for _, de := range m {
		entry := *de
		res[i] = &entry
		i++
	}
	cmd.result <- res
}

type cmdGetEntry struct {
	zid    id.Zid
	result chan<- resGetEntry
}
type resGetEntry = *directory.Entry

func (cmd *cmdGetEntry) run(m dirMap) {
	entry := m[cmd.zid]
	if entry == nil {
		cmd.result <- nil
	} else {
		result := *entry
		cmd.result <- &result
	}
}

type cmdNewEntry struct {
	result chan<- resNewEntry
}
type resNewEntry struct {
	entry *directory.Entry
	err   error
}

func (cmd *cmdNewEntry) run(m dirMap) {
	zid, err := place.GetNewZid(func(zid id.Zid) (bool, error) {
		_, ok := m[zid]
		return !ok, nil
	})
	if err != nil {
		cmd.result <- resNewEntry{nil, err}
		return
	}
	entry := &directory.Entry{Zid: zid}
	m[zid] = entry
	cmd.result <- resNewEntry{&directory.Entry{Zid: zid}, nil}
}

type cmdUpdateEntry struct {
	entry  *directory.Entry
	result chan<- struct{}
}

func (cmd *cmdUpdateEntry) run(m dirMap) {
	entry := *cmd.entry
	m[entry.Zid] = &entry
	cmd.result <- struct{}{}
}

type cmdRenameEntry struct {
	curEntry *directory.Entry
	newEntry *directory.Entry
	result   chan<- resRenameEntry
}

type resRenameEntry = error

func (cmd *cmdRenameEntry) run(m dirMap) {
	newEntry := *cmd.newEntry
	newZid := newEntry.Zid
	if _, found := m[newZid]; found {
		cmd.result <- &place.ErrInvalidID{Zid: newZid}
		return
	}
	delete(m, cmd.curEntry.Zid)
	m[newZid] = &newEntry
	cmd.result <- nil
}

type cmdDeleteEntry struct {
	zid    id.Zid
	result chan<- struct{}
}

func (cmd *cmdDeleteEntry) run(m dirMap) {
	delete(m, cmd.zid)
	cmd.result <- struct{}{}
}

Deleted place/dirplace/notifydir/watch.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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












































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package notifydir manages the notified directory part of a dirstore.
package notifydir

import (
	"os"
	"path/filepath"
	"regexp"
	"time"

	"github.com/fsnotify/fsnotify"

	"zettelstore.de/z/domain/id"
)

var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
}

type fileStatus int

const (
	fileStatusNone fileStatus = iota
	fileStatusReloadStart
	fileStatusReloadEnd
	fileStatusError
	fileStatusUpdate
	fileStatusDelete
)

type fileEvent struct {
	status fileStatus
	path   string // Full file path
	zid    id.Zid
	ext    string // File extension
	err    error  // Error if Status == fileStatusError
}

type sendResult int

const (
	sendDone sendResult = iota
	sendReload
	sendExit
)

func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) {
	defer close(events)

	var watcher *fsnotify.Watcher
	defer func() {
		if watcher != nil {
			watcher.Close()
		}
	}()

	sendEvent := func(ev *fileEvent) sendResult {
		select {
		case events <- ev:
		case _, ok := <-tick:
			if ok {
				return sendReload
			}
			return sendExit
		}
		return sendDone
	}

	sendError := func(err error) sendResult {
		return sendEvent(&fileEvent{status: fileStatusError, err: err})
	}

	sendFileEvent := func(status fileStatus, path string, match []string) sendResult {
		zid, err := id.Parse(match[1])
		if err != nil {
			return sendDone
		}
		event := &fileEvent{
			status: status,
			path:   path,
			zid:    zid,
			ext:    match[3],
		}
		return sendEvent(event)
	}

	reloadStartEvent := &fileEvent{status: fileStatusReloadStart}
	reloadEndEvent := &fileEvent{status: fileStatusReloadEnd}
	reloadFiles := func() bool {
		entries, err := os.ReadDir(directory)
		if err != nil {
			if res := sendError(err); res != sendDone {
				return res == sendReload
			}
			return true
		}

		if res := sendEvent(reloadStartEvent); res != sendDone {
			return res == sendReload
		}

		if watcher != nil {
			watcher.Close()
		}
		watcher, err = fsnotify.NewWatcher()
		if err != nil {
			if res := sendError(err); res != sendDone {
				return res == sendReload
			}
		}

		for _, entry := range entries {
			if entry.IsDir() {
				continue
			}
			if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() {
				continue
			}
			name := entry.Name()
			match := matchValidFileName(name)
			if len(match) > 0 {
				path := filepath.Join(directory, name)
				if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone {
					return res == sendReload
				}
			}
		}

		if watcher != nil {
			err = watcher.Add(directory)
			if err != nil {
				if res := sendError(err); res != sendDone {
					return res == sendReload
				}
			}
		}
		if res := sendEvent(reloadEndEvent); res != sendDone {
			return res == sendReload
		}
		return true
	}

	handleEvents := func() bool {
		const createOps = fsnotify.Create | fsnotify.Write
		const deleteOps = fsnotify.Remove | fsnotify.Rename

		for {
			select {
			case wevent, ok := <-watcher.Events:
				if !ok {
					return false
				}
				path := filepath.Clean(wevent.Name)
				match := matchValidFileName(filepath.Base(path))
				if len(match) == 0 {
					continue
				}
				if wevent.Op&createOps != 0 {
					if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() {
						continue
					}
					if res := sendFileEvent(
						fileStatusUpdate, path, match); res != sendDone {
						return res == sendReload
					}
				}
				if wevent.Op&deleteOps != 0 {
					if res := sendFileEvent(
						fileStatusDelete, path, match); res != sendDone {
						return res == sendReload
					}
				}
			case err, ok := <-watcher.Errors:
				if !ok {
					return false
				}
				if res := sendError(err); res != sendDone {
					return res == sendReload
				}
			case _, ok := <-tick:
				return ok
			}
		}
	}

	for {
		if !reloadFiles() {
			return
		}
		if watcher == nil {
			if _, ok := <-tick; !ok {
				return
			}
		} else {
			if !handleEvents() {
				return
			}
		}
	}
}

func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) {
	for _, ev := range events {
		if ev.status != fileStatusNone {
			out <- ev
		}
	}
}

func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent {
	switch ev.status {
	case fileStatusNone:
		return events
	case fileStatusReloadStart:
		events = events[0:0]
	case fileStatusUpdate, fileStatusDelete:
		if len(events) > 0 && mergeEvents(events, ev) {
			return events
		}
	}
	return append(events, ev)
}

func mergeEvents(events []*fileEvent, ev *fileEvent) bool {
	for i := len(events) - 1; i >= 0; i-- {
		oev := events[i]
		switch oev.status {
		case fileStatusReloadStart, fileStatusReloadEnd:
			return false
		case fileStatusUpdate, fileStatusDelete:
			if ev.path == oev.path {
				if ev.status == oev.status {
					return true
				}
				oev.status = fileStatusNone
				return false
			}
		}
	}
	return false
}

func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) {
	defer close(out)

	var sendTime time.Time
	sendTimeSet := false
	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()

	events := make([]*fileEvent, 0, 32)
	buffer := false
	for {
		select {
		case ev, ok := <-in:
			if !ok {
				sendCollectedEvents(out, events)
				return
			}
			if ev.status == fileStatusReloadStart {
				buffer = false
				events = events[0:0]
			}
			if buffer {
				if !sendTimeSet {
					sendTime = time.Now().Add(1500 * time.Millisecond)
					sendTimeSet = true
				}
				events = addEvent(events, ev)
				if len(events) > 1024 {
					sendCollectedEvents(out, events)
					events = events[0:0]
					sendTimeSet = false
				}
				continue
			}
			out <- ev
			if ev.status == fileStatusReloadEnd {
				buffer = true
			}
		case now := <-ticker.C:
			if sendTimeSet && now.After(sendTime) {
				sendCollectedEvents(out, events)
				events = events[0:0]
				sendTimeSet = false
			}
		}
	}
}

Deleted place/dirplace/notifydir/watch_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























































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package notifydir manages the notified directory part of a dirstore.
package notifydir

import "testing"

func sameStringSlices(sl1, sl2 []string) bool {
	if len(sl1) != len(sl2) {
		return false
	}
	for i := 0; i < len(sl1); i++ {
		if sl1[i] != sl2[i] {
			return false
		}
	}
	return true
}

func TestMatchValidFileName(t *testing.T) {
	testcases := []struct {
		name string
		exp  []string
	}{
		{"", []string{}},
		{".txt", []string{}},
		{"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}},
		{"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}},
		{"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}},
	}

	for i, tc := range testcases {
		got := matchValidFileName(tc.name)
		if len(got) == 0 {
			if len(tc.exp) > 0 {
				t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got)
			}
		} else {
			if got[0] != tc.name {
				t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got)
			}
			if !sameStringSlices(got[1:], tc.exp) {
				t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got)
			}
		}
	}
}

Deleted place/dirplace/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
55
56
57
58
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


































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package dirplace provides a directory-based zettel place.
package dirplace

import (
	"os"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/place/dirplace/directory"
	"zettelstore.de/z/place/fileplace"
)

func fileService(num uint32, cmds <-chan fileCmd) {
	for cmd := range cmds {
		cmd.run()
	}
}

type fileCmd interface {
	run()
}

// COMMAND: getMeta ----------------------------------------
//
// Retrieves the meta data from a zettel.

func getMeta(dp *dirPlace, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) {
	rc := make(chan resGetMeta)
	dp.getFileChan(zid) <- &fileGetMeta{entry, rc}
	res := <-rc
	close(rc)
	return res.meta, res.err
}

type fileGetMeta struct {
	entry *directory.Entry
	rc    chan<- resGetMeta
}
type resGetMeta struct {
	meta *meta.Meta
	err  error
}

func (cmd *fileGetMeta) run() {
	entry := cmd.entry
	var m *meta.Meta
	var err error
	switch entry.MetaSpec {
	case directory.MetaSpecFile:
		m, err = parseMetaFile(entry.Zid, entry.MetaPath)
	case directory.MetaSpecHeader:
		m, _, err = parseMetaContentFile(entry.Zid, entry.ContentPath)
	default:
		m = fileplace.CalcDefaultMeta(entry.Zid, entry.ContentExt)
	}
	if err == nil {
		cmdCleanupMeta(m, entry)
	}
	cmd.rc <- resGetMeta{m, err}
}

// COMMAND: getMetaContent ----------------------------------------
//
// Retrieves the meta data and the content of a zettel.

func getMetaContent(dp *dirPlace, entry *directory.Entry, zid id.Zid) (*meta.Meta, string, error) {
	rc := make(chan resGetMetaContent)
	dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc}
	res := <-rc
	close(rc)
	return res.meta, res.content, res.err
}

type fileGetMetaContent struct {
	entry *directory.Entry
	rc    chan<- resGetMetaContent
}
type resGetMetaContent struct {
	meta    *meta.Meta
	content string
	err     error
}

func (cmd *fileGetMetaContent) run() {
	var m *meta.Meta
	var content string
	var err error

	entry := cmd.entry
	switch entry.MetaSpec {
	case directory.MetaSpecFile:
		m, err = parseMetaFile(entry.Zid, entry.MetaPath)
		if err == nil {
			content, err = readFileContent(entry.ContentPath)
		}
	case directory.MetaSpecHeader:
		m, content, err = parseMetaContentFile(entry.Zid, entry.ContentPath)
	default:
		m = fileplace.CalcDefaultMeta(entry.Zid, entry.ContentExt)
		content, err = readFileContent(entry.ContentPath)
	}
	if err == nil {
		cmdCleanupMeta(m, entry)
	}
	cmd.rc <- resGetMetaContent{m, content, err}
}

// COMMAND: setZettel ----------------------------------------
//
// Writes a new or exsting zettel.

func setZettel(dp *dirPlace, entry *directory.Entry, zettel domain.Zettel) error {
	rc := make(chan resSetZettel)
	dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc}
	err := <-rc
	close(rc)
	return err
}

type fileSetZettel struct {
	entry  *directory.Entry
	zettel domain.Zettel
	rc     chan<- resSetZettel
}
type resSetZettel = error

func (cmd *fileSetZettel) run() {
	var err error
	switch cmd.entry.MetaSpec {
	case directory.MetaSpecFile:
		err = cmd.runMetaSpecFile()
	case directory.MetaSpecHeader:
		err = cmd.runMetaSpecHeader()
	case directory.MetaSpecNone:
		// TODO: if meta has some additional infos: write meta to new .meta;
		// update entry in dir
		err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())
	default:
		panic("TODO: ???")
	}
	cmd.rc <- err
}

func (cmd *fileSetZettel) runMetaSpecFile() error {
	f, err := openFileWrite(cmd.entry.MetaPath)
	if err == nil {
		err = writeFileZid(f, cmd.zettel.Meta.Zid)
		if err == nil {
			_, err = cmd.zettel.Meta.Write(f, true)
			if err1 := f.Close(); err == nil {
				err = err1
			}
			if err == nil {
				err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())
			}
		}
	}
	return err
}

func (cmd *fileSetZettel) runMetaSpecHeader() error {
	f, err := openFileWrite(cmd.entry.ContentPath)
	if err == nil {
		err = writeFileZid(f, cmd.zettel.Meta.Zid)
		if err == nil {
			_, err = cmd.zettel.Meta.WriteAsHeader(f, true)
			if err == nil {
				_, err = f.WriteString(cmd.zettel.Content.AsString())
				if err1 := f.Close(); err == nil {
					err = err1
				}
			}
		}
	}
	return err
}

// COMMAND: deleteZettel ----------------------------------------
//
// Deletes an existing zettel.

func deleteZettel(dp *dirPlace, entry *directory.Entry, zid id.Zid) error {
	rc := make(chan resDeleteZettel)
	dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc}
	err := <-rc
	close(rc)
	return err
}

type fileDeleteZettel struct {
	entry *directory.Entry
	rc    chan<- resDeleteZettel
}
type resDeleteZettel = error

func (cmd *fileDeleteZettel) run() {
	var err error

	switch cmd.entry.MetaSpec {
	case directory.MetaSpecFile:
		err1 := os.Remove(cmd.entry.MetaPath)
		err = os.Remove(cmd.entry.ContentPath)
		if err == nil {
			err = err1
		}
	case directory.MetaSpecHeader:
		err = os.Remove(cmd.entry.ContentPath)
	case directory.MetaSpecNone:
		err = os.Remove(cmd.entry.ContentPath)
	default:
		panic("TODO: ???")
	}
	cmd.rc <- err
}

// Utility functions ----------------------------------------

func readFileContent(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) {
	src, err := readFileContent(path)
	if err != nil {
		return nil, err
	}
	inp := input.NewInput(src)
	return meta.NewFromInput(zid, inp), nil
}

func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) {
	src, err := readFileContent(path)
	if err != nil {
		return nil, "", err
	}
	inp := input.NewInput(src)
	meta := meta.NewFromInput(zid, inp)
	return meta, src[inp.Pos:], nil
}

func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) {
	fileplace.CleanupMeta(
		m,
		entry.Zid, entry.ContentExt,
		entry.MetaSpec == directory.MetaSpecFile,
		entry.Duplicates,
	)
}

func openFileWrite(path string) (*os.File, error) {
	return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
}

func writeFileZid(f *os.File, zid id.Zid) error {
	_, err := f.WriteString("id: ")
	if err == nil {
		_, err = f.Write(zid.Bytes())
		if err == nil {
			_, err = f.WriteString("\n")
		}
	}
	return err
}

func writeFileContent(path string, content string) error {
	f, err := openFileWrite(path)
	if err == nil {
		_, err = f.WriteString(content)
		if err1 := f.Close(); err == nil {
			err = err1
		}
	}
	return err
}

Deleted place/dirplace/simpledir/simpledir.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

























































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package simpledir manages the directory part of a dirstore.
package simpledir

import (
	"os"
	"path/filepath"
	"regexp"
	"sync"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/dirplace/directory"
)

// simpleService specifies a directory service without scanning.
type simpleService struct {
	dirPath string
	mx      sync.Mutex
}

// NewService creates a new directory service.
func NewService(directoryPath string) directory.Service {
	return &simpleService{
		dirPath: directoryPath,
	}
}

func (ss *simpleService) Start() error {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	_, err := os.ReadDir(ss.dirPath)
	return err
}

func (ss *simpleService) Stop() error {
	return nil
}

func (ss *simpleService) NumEntries() (int, error) {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	entries, err := ss.getEntries()
	if err == nil {
		return len(entries), nil
	}
	return 0, err
}

func (ss *simpleService) GetEntries() ([]*directory.Entry, error) {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	entrySet, err := ss.getEntries()
	if err != nil {
		return nil, err
	}
	result := make([]*directory.Entry, 0, len(entrySet))
	for _, entry := range entrySet {
		result = append(result, entry)
	}
	return result, nil
}
func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) {
	dirEntries, err := os.ReadDir(ss.dirPath)
	if err != nil {
		return nil, err
	}
	entrySet := make(map[id.Zid]*directory.Entry)
	for _, dirEntry := range dirEntries {
		if dirEntry.IsDir() {
			continue
		}
		if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() {
			continue
		}
		name := dirEntry.Name()
		match := matchValidFileName(name)
		if len(match) == 0 {
			continue
		}
		zid, err := id.Parse(match[1])
		if err != nil {
			continue
		}
		var entry *directory.Entry
		if e, ok := entrySet[zid]; ok {
			entry = e
		} else {
			entry = &directory.Entry{Zid: zid}
			entrySet[zid] = entry
		}
		updateEntry(entry, filepath.Join(ss.dirPath, name), match[3])
	}
	return entrySet, nil
}

var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
}

func updateEntry(entry *directory.Entry, path, ext string) {
	if ext == "meta" {
		entry.MetaSpec = directory.MetaSpecFile
		entry.MetaPath = path
	} else if entry.ContentExt != "" && entry.ContentExt != ext {
		entry.Duplicates = true
	} else {
		if entry.MetaSpec != directory.MetaSpecFile {
			if ext == "zettel" {
				entry.MetaSpec = directory.MetaSpecHeader
			} else {
				entry.MetaSpec = directory.MetaSpecNone
			}
		}
		entry.ContentPath = path
		entry.ContentExt = ext
	}
}

func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	return ss.getEntry(zid)
}
func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) {
	pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*"
	paths, err := filepath.Glob(pattern)
	if err != nil {
		return nil, err
	}
	if len(paths) == 0 {
		return nil, nil
	}
	entry := &directory.Entry{Zid: zid}
	for _, path := range paths {
		ext := filepath.Ext(path)
		if len(ext) > 0 && ext[0] == '.' {
			ext = ext[1:]
		}
		updateEntry(entry, path, ext)
	}
	return entry, nil
}

func (ss *simpleService) GetNew() (*directory.Entry, error) {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	zid, err := place.GetNewZid(func(zid id.Zid) (bool, error) {
		entry, err := ss.getEntry(zid)
		if err != nil {
			return false, nil
		}
		return !entry.IsValid(), nil
	})
	if err != nil {
		return nil, err
	}
	return &directory.Entry{Zid: zid}, nil
}

func (ss *simpleService) UpdateEntry(entry *directory.Entry) error {
	// Noting to to, since the actual file update is done by dirplace
	return nil
}

func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error {
	// Noting to to, since the actual file rename is done by dirplace
	return nil
}

func (ss *simpleService) DeleteEntry(zid id.Zid) error {
	// Noting to to, since the actual file delete is done by dirplace
	return nil
}

Deleted place/fileplace/fileplace.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package fileplace provides places that are stored in a file.
package fileplace

import (
	"errors"
	"net/url"
	"path/filepath"
	"strings"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"
)

func init() {
	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) {
		path := getFilepathFromURL(u)
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".zip" {
			return nil, errors.New("unknown extension '" + ext + "' in place URL: " + u.String())
		}
		return &zipPlace{name: path, enricher: cdata.Enricher}, nil
	})
}

func getFilepathFromURL(u *url.URL) string {
	name := u.Opaque
	if name == "" {
		name = u.Path
	}
	components := strings.Split(name, "/")
	fileName := filepath.Join(components...)
	if len(components) > 0 && components[0] == "" {
		return "/" + fileName
	}
	return fileName
}

var alternativeSyntax = map[string]string{
	"htm": "html",
}

func calculateSyntax(ext string) string {
	ext = strings.ToLower(ext)
	if syntax, ok := alternativeSyntax[ext]; ok {
		return syntax
	}
	return ext
}

// CalcDefaultMeta returns metadata with default values for the given entry.
func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta {
	m := meta.New(zid)
	m.Set(meta.KeyTitle, zid.String())
	m.Set(meta.KeySyntax, calculateSyntax(ext))
	return m
}

// CleanupMeta enhances the given metadata.
func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta, duplicates bool) {
	if title, ok := m.Get(meta.KeyTitle); !ok || title == "" {
		m.Set(meta.KeyTitle, zid.String())
	}

	if inMeta {
		if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
			dm := CalcDefaultMeta(zid, ext)
			syntax, ok = dm.Get(meta.KeySyntax)
			if !ok {
				panic("Default meta must contain syntax")
			}
			m.Set(meta.KeySyntax, syntax)
		}
	}

	if duplicates {
		m.Set(meta.KeyDuplicates, meta.ValueTrue)
	}
}

Deleted place/fileplace/zipplace.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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




































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package fileplace provides places that are stored in a file.
package fileplace

import (
	"archive/zip"
	"context"
	"io"
	"regexp"
	"strings"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
)

var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
}

type zipEntry struct {
	metaName     string
	contentName  string
	contentExt   string // (normalized) file extension of zettel content
	metaInHeader bool
}

type zipPlace struct {
	name     string
	enricher place.Enricher
	zettel   map[id.Zid]*zipEntry // no lock needed, because read-only after creation
}

func (zp *zipPlace) Location() string {
	if strings.HasPrefix(zp.name, "/") {
		return "file://" + zp.name
	}
	return "file:" + zp.name
}

func (zp *zipPlace) Start(ctx context.Context) error {
	reader, err := zip.OpenReader(zp.name)
	if err != nil {
		return err
	}
	defer reader.Close()
	zp.zettel = make(map[id.Zid]*zipEntry)
	for _, f := range reader.File {
		match := matchValidFileName(f.Name)
		if len(match) < 1 {
			continue
		}
		zid, err := id.Parse(match[1])
		if err != nil {
			continue
		}
		zp.addFile(zid, f.Name, match[3])
	}
	return nil
}

func (zp *zipPlace) addFile(zid id.Zid, name, ext string) {
	entry := zp.zettel[zid]
	if entry == nil {
		entry = &zipEntry{}
		zp.zettel[zid] = entry
	}
	switch ext {
	case "zettel":
		if entry.contentExt == "" {
			entry.contentName = name
			entry.contentExt = ext
			entry.metaInHeader = true
		}
	case "meta":
		entry.metaName = name
		entry.metaInHeader = false
	default:
		if entry.contentExt == "" {
			entry.contentExt = ext
			entry.contentName = name
		}
	}
}

func (zp *zipPlace) Stop(ctx context.Context) error {
	zp.zettel = nil
	return nil
}

func (zp *zipPlace) CanCreateZettel(ctx context.Context) bool { return false }

func (zp *zipPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	return id.Invalid, place.ErrReadOnly
}

func (zp *zipPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	entry, ok := zp.zettel[zid]
	if !ok {
		return domain.Zettel{}, place.ErrNotFound
	}
	reader, err := zip.OpenReader(zp.name)
	if err != nil {
		return domain.Zettel{}, err
	}
	defer reader.Close()

	var m *meta.Meta
	var src string
	var inMeta bool
	if entry.metaInHeader {
		src, err = readZipFileContent(reader, entry.contentName)
		if err != nil {
			return domain.Zettel{}, err
		}
		inp := input.NewInput(src)
		m = meta.NewFromInput(zid, inp)
		src = src[inp.Pos:]
	} else if metaName := entry.metaName; metaName != "" {
		m, err = readZipMetaFile(reader, zid, metaName)
		if err != nil {
			return domain.Zettel{}, err
		}
		src, err = readZipFileContent(reader, entry.contentName)
		if err != nil {
			return domain.Zettel{}, err
		}
		inMeta = true
	} else {
		m = CalcDefaultMeta(zid, entry.contentExt)
	}
	CleanupMeta(m, zid, entry.contentExt, inMeta, false)
	return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil
}

func (zp *zipPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	entry, ok := zp.zettel[zid]
	if !ok {
		return nil, place.ErrNotFound
	}
	reader, err := zip.OpenReader(zp.name)
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	return readZipMeta(reader, zid, entry)
}

func (zp *zipPlace) FetchZids(ctx context.Context) (id.Set, error) {
	result := id.NewSetCap(len(zp.zettel))
	for zid := range zp.zettel {
		result[zid] = true
	}
	return result, nil
}

func (zp *zipPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
	reader, err := zip.OpenReader(zp.name)
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	for zid, entry := range zp.zettel {
		m, err := readZipMeta(reader, zid, entry)
		if err != nil {
			continue
		}
		zp.enricher.Enrich(ctx, m)
		if match(m) {
			res = append(res, m)
		}
	}
	return res, nil
}

func (zp *zipPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	return false
}

func (zp *zipPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	return place.ErrReadOnly
}

func (zp *zipPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	_, ok := zp.zettel[zid]
	return !ok
}

func (zp *zipPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if _, ok := zp.zettel[curZid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}

func (zp *zipPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false }

func (zp *zipPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := zp.zettel[zid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}

func (zp *zipPlace) ReadStats(st *place.ManagedPlaceStats) {
	st.ReadOnly = true
	st.Zettel = len(zp.zettel)
}

func readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *zipEntry) (m *meta.Meta, err error) {
	var inMeta bool
	if entry.metaInHeader {
		m, err = readZipMetaFile(reader, zid, entry.contentName)
	} else if metaName := entry.metaName; metaName != "" {
		m, err = readZipMetaFile(reader, zid, entry.metaName)
		inMeta = true
	} else {
		m = CalcDefaultMeta(zid, entry.contentExt)
	}
	if err == nil {
		CleanupMeta(m, zid, entry.contentExt, inMeta, false)
	}
	return m, err
}

func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) {
	src, err := readZipFileContent(reader, name)
	if err != nil {
		return nil, err
	}
	inp := input.NewInput(src)
	return meta.NewFromInput(zid, inp), nil
}

func readZipFileContent(reader *zip.ReadCloser, name string) (string, error) {
	f, err := reader.Open(name)
	if err != nil {
		return "", err
	}
	defer f.Close()
	buf, err := io.ReadAll(f)
	if err != nil {
		return "", err
	}
	return string(buf), nil
}

Deleted place/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
37





































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package place provides a generic interface to zettel places.
package place

import (
	"time"

	"zettelstore.de/z/domain/id"
)

// GetNewZid calculates a new and unused zettel identifier, based on the current date and time.
func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) {
	withSeconds := false
	for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout)
		zid := id.New(withSeconds)
		found, err := testZid(zid)
		if err != nil {
			return id.Invalid, err
		}
		if found {
			return zid, nil
		}
		// TODO: do not wait here unconditionally.
		time.Sleep(100 * time.Millisecond)
		withSeconds = true
	}
	return id.Invalid, ErrConflict
}

Deleted place/manager/anteroom.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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



























































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various places and indexes of a Zettelstore.
package manager

import (
	"sync"

	"zettelstore.de/z/domain/id"
)

type arAction int

const (
	arNothing arAction = iota
	arReload
	arUpdate
	arDelete
)

type anteroom struct {
	next    *anteroom
	waiting map[id.Zid]arAction
	curLoad int
	reload  bool
}

type anterooms struct {
	mx      sync.Mutex
	first   *anteroom
	last    *anteroom
	maxLoad int
}

func newAnterooms(maxLoad int) *anterooms {
	return &anterooms{maxLoad: maxLoad}
}

func (ar *anterooms) Enqueue(zid id.Zid, action arAction) {
	if !zid.IsValid() || action == arNothing || action == arReload {
		return
	}
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		ar.first = ar.makeAnteroom(zid, action)
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not place zettel in reload room
		}
		a, ok := room.waiting[zid]
		if !ok {
			continue
		}
		switch action {
		case a:
			return
		case arUpdate:
			room.waiting[zid] = action
		case arDelete:
			room.waiting[zid] = action
		}
		return
	}
	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
		room.waiting[zid] = action
		room.curLoad++
		return
	}
	room := ar.makeAnteroom(zid, action)
	ar.last.next = room
	ar.last = room
}

func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom {
	c := ar.maxLoad
	if c == 0 {
		c = 100
	}
	waiting := make(map[id.Zid]arAction, c)
	waiting[zid] = action
	return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false}
}

func (ar *anterooms) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = ar.makeAnteroom(id.Invalid, arReload)
	ar.last = ar.first
}

func (ar *anterooms) Reload(delZids id.Slice, newZids id.Set) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	delWaiting := createWaitingSlice(delZids, arDelete)
	newWaiting := createWaitingSet(newZids, arUpdate)
	ar.deleteReloadedRooms()

	if ds := len(delWaiting); ds > 0 {
		if ns := len(newWaiting); ns > 0 {
			roomNew := &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns, reload: true}
			ar.first = &anteroom{next: roomNew, waiting: delWaiting, curLoad: ds, reload: true}
			if roomNew.next == nil {
				ar.last = roomNew
			}
			return
		}

		ar.first = &anteroom{next: ar.first, waiting: delWaiting, curLoad: ds}
		if ar.first.next == nil {
			ar.last = ar.first
		}
		return
	}

	if ns := len(newWaiting); ns > 0 {
		ar.first = &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns}
		if ar.first.next == nil {
			ar.last = ar.first
		}
		return
	}

	ar.first = nil
	ar.last = nil
}

func createWaitingSlice(zids id.Slice, action arAction) map[id.Zid]arAction {
	waitingSet := make(map[id.Zid]arAction, len(zids))
	for _, zid := range zids {
		if zid.IsValid() {
			waitingSet[zid] = action
		}
	}
	return waitingSet
}

func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction {
	waitingSet := make(map[id.Zid]arAction, len(zids))
	for zid := range zids {
		if zid.IsValid() {
			waitingSet[zid] = action
		}
	}
	return waitingSet
}

func (ar *anterooms) deleteReloadedRooms() {
	room := ar.first
	for room != nil && room.reload {
		room = room.next
	}
	ar.first = room
	if room == nil {
		ar.last = nil
	}
}

func (ar *anterooms) Dequeue() (arAction, id.Zid) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		return arNothing, id.Invalid
	}
	for zid, action := range ar.first.waiting {
		delete(ar.first.waiting, zid)
		if len(ar.first.waiting) == 0 {
			ar.first = ar.first.next
			if ar.first == nil {
				ar.last = nil
			}
		}
		return action, zid
	}
	return arNothing, id.Invalid
}

Deleted place/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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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


























































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various places and indexes of a Zettelstore.
package manager

import (
	"testing"

	"zettelstore.de/z/domain/id"
)

func TestSimple(t *testing.T) {
	ar := newAnterooms(2)
	ar.Enqueue(id.Zid(1), arUpdate)
	action, zid := ar.Dequeue()
	if zid != id.Zid(1) || action != arUpdate {
		t.Errorf("Expected 1/arUpdate, but got %v/%v", zid, action)
	}
	action, zid = ar.Dequeue()
	if zid != id.Invalid && action != arDelete {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.Enqueue(id.Zid(1), arUpdate)
	ar.Enqueue(id.Zid(2), arUpdate)
	if ar.first != ar.last {
		t.Errorf("Expected one room, but got more")
	}
	ar.Enqueue(id.Zid(3), arUpdate)
	if ar.first == ar.last {
		t.Errorf("Expected more than one room, but got only one")
	}

	count := 0
	for ; count < 1000; count++ {
		action, _ := ar.Dequeue()
		if action == arNothing {
			break
		}
	}
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	ar := newAnterooms(1)
	ar.Enqueue(id.Zid(1), arUpdate)
	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.Slice{2}, id.NewSet(3, 4))
	ar.Enqueue(id.Zid(5), arUpdate)
	ar.Enqueue(id.Zid(5), arDelete)
	ar.Enqueue(id.Zid(5), arDelete)
	ar.Enqueue(id.Zid(5), arUpdate)
	if ar.first == ar.last || ar.first.next == ar.last || ar.first.next.next != ar.last {
		t.Errorf("Expected 3 rooms")
	}
	action, zid = ar.Dequeue()
	if zid != id.Zid(2) || action != arDelete {
		t.Errorf("Expected 2/arDelete, but got %v/%v", zid, action)
	}
	action, zid1 := ar.Dequeue()
	if action != arUpdate {
		t.Errorf("Expected arUpdate, but got %v", action)
	}
	action, zid2 := ar.Dequeue()
	if action != arUpdate {
		t.Errorf("Expected arUpdate, but got %v", action)
	}
	if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) {
		t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2)
	}
	action, zid = ar.Dequeue()
	if zid != id.Zid(5) || action != arUpdate {
		t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action)
	}
	action, zid = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnterooms(1)
	ar.Reload(nil, id.NewSet(id.Zid(6)))
	action, zid = ar.Dequeue()
	if zid != id.Zid(6) || action != arUpdate {
		t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action)
	}
	action, zid = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnterooms(1)
	ar.Reload(id.Slice{7}, nil)
	action, zid = ar.Dequeue()
	if zid != id.Zid(7) || action != arDelete {
		t.Errorf("Expected 7/arDelete, 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.Enqueue(id.Zid(8), arUpdate)
	ar.Reload(nil, nil)
	action, zid = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}
}

Deleted place/manager/collect.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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

















































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various places and indexes of a Zettelstore.
package manager

import (
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/place/manager/store"
	"zettelstore.de/z/strfun"
)

type collectData struct {
	refs  id.Set
	words store.WordSet
	urls  store.WordSet
}

func (data *collectData) initialize() {
	data.refs = id.NewSet()
	data.words = store.NewWordSet()
	data.urls = store.NewWordSet()
}

func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) {
	ast.NewTopDownTraverser(data).VisitBlockSlice(zn.Ast)
}

func collectInlineIndexData(ins ast.InlineSlice, data *collectData) {
	ast.NewTopDownTraverser(data).VisitInlineSlice(ins)
}

// VisitVerbatim collects the verbatim text in the word set.
func (data *collectData) VisitVerbatim(vn *ast.VerbatimNode) {
	for _, line := range vn.Lines {
		data.addText(line)
	}
}

// VisitRegion does nothing.
func (data *collectData) VisitRegion(rn *ast.RegionNode) {}

// VisitHeading does nothing.
func (data *collectData) VisitHeading(hn *ast.HeadingNode) {}

// VisitHRule does nothing.
func (data *collectData) VisitHRule(hn *ast.HRuleNode) {}

// VisitList does nothing.
func (data *collectData) VisitNestedList(ln *ast.NestedListNode) {}

// VisitDescriptionList does nothing.
func (data *collectData) VisitDescriptionList(dn *ast.DescriptionListNode) {}

// VisitPara does nothing.
func (data *collectData) VisitPara(pn *ast.ParaNode) {}

// VisitTable does nothing.
func (data *collectData) VisitTable(tn *ast.TableNode) {}

// VisitBLOB does nothing.
func (data *collectData) VisitBLOB(bn *ast.BLOBNode) {}

// VisitText collects the text in the word set.
func (data *collectData) VisitText(tn *ast.TextNode) {
	data.addText(tn.Text)
}

// VisitTag collects the tag name in the word set.
func (data *collectData) VisitTag(tn *ast.TagNode) {
	data.addText(tn.Tag)
}

// VisitSpace does nothing.
func (data *collectData) VisitSpace(sn *ast.SpaceNode) {}

// VisitBreak does nothing.
func (data *collectData) VisitBreak(bn *ast.BreakNode) {}

// VisitLink collects the given link as a reference.
func (data *collectData) VisitLink(ln *ast.LinkNode) {
	ref := ln.Ref
	if ref == nil {
		return
	}
	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] = true
	}
}

// VisitImage collects the image links as a reference.
func (data *collectData) VisitImage(in *ast.ImageNode) {
	ref := in.Ref
	if ref == nil {
		return
	}
	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] = true
	}
}

// VisitCite does nothing.
func (data *collectData) VisitCite(cn *ast.CiteNode) {}

// VisitFootnote does nothing.
func (data *collectData) VisitFootnote(fn *ast.FootnoteNode) {}

// VisitMark does nothing.
func (data *collectData) VisitMark(mn *ast.MarkNode) {}

// VisitFormat does nothing.
func (data *collectData) VisitFormat(fn *ast.FormatNode) {}

// VisitLiteral collects the literal words in the word set.
func (data *collectData) VisitLiteral(ln *ast.LiteralNode) {
	data.addText(ln.Text)
}

func (data *collectData) addText(s string) {
	for _, word := range strfun.NormalizeWords(s) {
		data.words.Add(word)
	}
}

Deleted place/manager/enrich.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50


















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various places and indexes of a Zettelstore.
package manager

import (
	"context"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
)

// Enrich computes additional properties and updates the given metadata.
func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta) {
	if place.DoNotEnrich(ctx) {
		// Enrich is called indirectly via indexer or enrichment is not requested
		// because of other reasons -> ignore this call, do not update meta data
		return
	}
	computePublished(m)
	mgr.idxStore.Enrich(ctx, m)
}

func computePublished(m *meta.Meta) {
	if _, ok := m.Get(meta.KeyPublished); ok {
		return
	}
	if modified, ok := m.Get(meta.KeyModified); ok {
		if _, ok = meta.TimeValue(modified); ok {
			m.Set(meta.KeyPublished, modified)
			return
		}
	}
	zid := m.Zid.String()
	if _, ok := meta.TimeValue(zid); ok {
		m.Set(meta.KeyPublished, zid)
		return
	}

	// Neither the zettel was modified nor the zettel identifer contains a valid
	// timestamp. In this case do not set the "published" property.
}

Deleted place/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
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



























































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various places and indexes of a Zettelstore.
package manager

import (
	"context"
	"net/url"
	"time"

	"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/place"
	"zettelstore.de/z/place/manager/store"
	"zettelstore.de/z/strfun"
)

// SelectEqual all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SelectEqual(word string) id.Set {
	return mgr.idxStore.SelectEqual(word)
}

// SelectPrefix 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) SelectPrefix(prefix string) id.Set {
	return mgr.idxStore.SelectPrefix(prefix)
}

// SelectSuffix 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) SelectSuffix(suffix string) id.Set {
	return mgr.idxStore.SelectSuffix(suffix)
}

// SelectContains all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SelectContains(s string) id.Set {
	return mgr.idxStore.SelectContains(s)
}

// idxIndexer runs in the background and updates the index data structures.
// This is the main service of the idxIndexer.
func (mgr *Manager) idxIndexer() {
	// Something may panic. Ensure a running indexer.
	defer func() {
		if r := recover(); r != nil {
			kernel.Main.LogRecover("Indexer", r)
			go mgr.idxIndexer()
		}
	}()

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := place.NoEnrichContext(context.Background())
	for {
		start := time.Now()
		if mgr.idxWorkService(ctx) {
			mgr.idxMx.Lock()
			mgr.idxDurLastIndex = time.Since(start)
			mgr.idxMx.Unlock()
		}
		if !mgr.idxSleepService(timer, timerDuration) {
			return
		}
	}
}

func (mgr *Manager) idxWorkService(ctx context.Context) bool {
	changed := false
	for {
		switch action, zid := mgr.idxAr.Dequeue(); action {
		case arNothing:
			return changed
		case arReload:
			zids, err := mgr.FetchZids(ctx)
			if err == nil {
				mgr.idxAr.Reload(nil, zids)
				mgr.idxMx.Lock()
				mgr.idxLastReload = time.Now()
				mgr.idxSinceReload = 0
				mgr.idxMx.Unlock()
			}
		case arUpdate:
			changed = true
			mgr.idxMx.Lock()
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()
			zettel, err := mgr.GetZettel(ctx, zid)
			if err != nil {
				// TODO: on some errors put the zid into a "try later" set
				continue
			}
			mgr.idxUpdateZettel(ctx, zettel)
		case arDelete:
			changed = true
			mgr.idxMx.Lock()
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()
			mgr.idxDeleteZettel(zid)
		}
	}
}

func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool {
	select {
	case _, ok := <-mgr.idxReady:
		if !ok {
			return false
		}
	case _, ok := <-timer.C:
		if !ok {
			return false
		}
		timer.Reset(timerDuration)
	case <-mgr.done:
		if !timer.Stop() {
			<-timer.C
		}
		return false
	}
	return true
}

func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) {
	m := zettel.Meta
	if m.GetBool(meta.KeyNoIndex) {
		// Zettel maybe in index
		toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid)
		mgr.idxCheckZettel(toCheck)
		return
	}

	var cData collectData
	cData.initialize()
	collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData)
	zi := store.NewZettelIndex(m.Zid)
	mgr.idxCollectFromMeta(ctx, m, zi, &cData)
	mgr.idxProcessData(ctx, zi, &cData)
	toCheck := mgr.idxStore.UpdateReferences(ctx, zi)
	mgr.idxCheckZettel(toCheck)
}

func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
	for _, pair := range m.Pairs(false) {
		descr := meta.GetDescription(pair.Key)
		if descr.IsComputed() {
			continue
		}
		switch descr.Type {
		case meta.TypeID:
			mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi)
		case meta.TypeIDSet:
			for _, val := range meta.ListFromValue(pair.Value) {
				mgr.idxUpdateValue(ctx, descr.Inverse, val, zi)
			}
		case meta.TypeZettelmarkup:
			collectInlineIndexData(parser.ParseMetadata(pair.Value), cData)
		case meta.TypeURL:
			if _, err := url.Parse(pair.Value); err == nil {
				cData.urls.Add(pair.Value)
			}
		default:
			for _, word := range strfun.NormalizeWords(pair.Value) {
				cData.words.Add(word)
			}
		}
	}
}

func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
	for ref := range cData.refs {
		if _, err := mgr.GetMeta(ctx, ref); err == nil {
			zi.AddBackRef(ref)
		} else {
			zi.AddDeadRef(ref)
		}
	}
	zi.SetWords(cData.words)
	zi.SetUrls(cData.urls)
}

func (mgr *Manager) idxUpdateValue(ctx context.Context, inverse string, value string, zi *store.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if _, err := mgr.GetMeta(ctx, zid); err != nil {
		zi.AddDeadRef(zid)
		return
	}
	if inverse == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddMetaRef(inverse, zid)
}

func (mgr *Manager) idxDeleteZettel(zid id.Zid) {
	toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid)
	mgr.idxCheckZettel(toCheck)
}

func (mgr *Manager) idxCheckZettel(s id.Set) {
	for zid := range s {
		mgr.idxAr.Enqueue(zid, arUpdate)
	}
}

Deleted place/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
47
48
49
50
51
52
53
54
55
56
57
58
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






















































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various places and indexes of a Zettelstore.
package manager

import (
	"context"
	"io"
	"log"
	"net/url"
	"sort"
	"sync"
	"time"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager/memstore"
	"zettelstore.de/z/place/manager/store"
)

// ConnectData contains all administration related values.
type ConnectData struct {
	Config   config.Config
	Enricher place.Enricher
	Notify   chan<- place.UpdateInfo
}

// Connect returns a handle to the specified place
func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (place.ManagedPlace, error) {
	if authManager.IsReadonly() {
		rawURL := u.String()
		// TODO: the following is wrong under some circumstances:
		// 1. fragment is set
		if q := u.Query(); len(q) == 0 {
			rawURL += "?readonly"
		} else if _, ok := q["readonly"]; !ok {
			rawURL += "&readonly"
		}
		var err error
		if u, err = url.Parse(rawURL); err != nil {
			return nil, err
		}
	}

	if create, ok := registry[u.Scheme]; ok {
		return create(u, cdata)
	}
	return nil, &ErrInvalidScheme{u.Scheme}
}

// ErrInvalidScheme is returned if there is no place with the given scheme
type ErrInvalidScheme struct{ Scheme string }

func (err *ErrInvalidScheme) Error() string { return "Invalid scheme: " + err.Scheme }

type createFunc func(*url.URL, *ConnectData) (place.ManagedPlace, error)

var registry = map[string]createFunc{}

// Register the encoder for later retrieval.
func Register(scheme string, create createFunc) {
	if _, ok := registry[scheme]; ok {
		log.Fatalf("Place with scheme %q already registered", scheme)
	}
	registry[scheme] = create
}

// GetSchemes returns all registered scheme, ordered by scheme string.
func GetSchemes() []string {
	result := make([]string, 0, len(registry))
	for scheme := range registry {
		result = append(result, scheme)
	}
	sort.Strings(result)
	return result
}

// Manager is a coordinating place.
type Manager struct {
	mgrMx        sync.RWMutex
	started      bool
	rtConfig     config.Config
	subplaces    []place.ManagedPlace
	observers    []place.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan place.UpdateInfo
	propertyKeys map[string]bool // Set of property key names

	// Indexer data
	idxStore store.Store
	idxAr    *anterooms
	idxReady chan struct{} // Signal a non-empty anteroom to background task

	// Indexer stats data
	idxMx           sync.RWMutex
	idxLastReload   time.Time
	idxSinceReload  uint64
	idxDurLastIndex time.Duration
}

// New creates a new managing place.
func New(placeURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) {
	propertyKeys := make(map[string]bool)
	for _, kd := range meta.GetSortedKeyDescriptions() {
		if kd.IsProperty() {
			propertyKeys[kd.Name] = true
		}
	}
	mgr := &Manager{
		rtConfig:     rtConfig,
		infos:        make(chan place.UpdateInfo, len(placeURIs)*10),
		propertyKeys: propertyKeys,

		idxStore: memstore.New(),
		idxAr:    newAnterooms(10),
		idxReady: make(chan struct{}, 1),
	}
	cdata := ConnectData{Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	subplaces := make([]place.ManagedPlace, 0, len(placeURIs)+2)
	for _, uri := range placeURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
			return nil, err
		}
		if p != nil {
			subplaces = append(subplaces, p)
		}
	}
	constplace, err := registry[" const"](nil, &cdata)
	if err != nil {
		return nil, err
	}
	progplace, err := registry[" prog"](nil, &cdata)
	if err != nil {
		return nil, err
	}
	subplaces = append(subplaces, constplace, progplace)
	mgr.subplaces = subplaces
	return mgr, nil
}

// RegisterObserver registers an observer that will be notified
// if a zettel was found to be changed.
func (mgr *Manager) RegisterObserver(f place.UpdateFunc) {
	if f != nil {
		mgr.mxObserver.Lock()
		mgr.observers = append(mgr.observers, f)
		mgr.mxObserver.Unlock()
	}
}

func (mgr *Manager) notifyObserver(ci *place.UpdateInfo) {
	mgr.mxObserver.RLock()
	observers := mgr.observers
	mgr.mxObserver.RUnlock()
	for _, ob := range observers {
		ob(*ci)
	}
}

func (mgr *Manager) notifier() {
	// The call to notify may panic. Ensure a running notifier.
	defer func() {
		if r := recover(); r != nil {
			kernel.Main.LogRecover("Notifier", r)
			go mgr.notifier()
		}
	}()

	for {
		select {
		case ci, ok := <-mgr.infos:
			if ok {
				mgr.idxEnqueue(ci.Reason, ci.Zid)
				if ci.Place == nil {
					ci.Place = mgr
				}
				mgr.notifyObserver(&ci)
			}
		case <-mgr.done:
			return
		}
	}
}

func (mgr *Manager) idxEnqueue(reason place.UpdateReason, zid id.Zid) {
	switch reason {
	case place.OnReload:
		mgr.idxAr.Reset()
	case place.OnUpdate:
		mgr.idxAr.Enqueue(zid, arUpdate)
	case place.OnDelete:
		mgr.idxAr.Enqueue(zid, arDelete)
	default:
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}

// Start the place. Now all other functions of the place are allowed.
// Starting an already started place is not allowed.
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mgrMx.Lock()
	if mgr.started {
		mgr.mgrMx.Unlock()
		return place.ErrStarted
	}
	for i := len(mgr.subplaces) - 1; i >= 0; i-- {
		ssi, ok := mgr.subplaces[i].(place.StartStopper)
		if !ok {
			continue
		}
		err := ssi.Start(ctx)
		if err == nil {
			continue
		}
		for j := i + 1; j < len(mgr.subplaces); j++ {
			if ssj, ok := mgr.subplaces[j].(place.StartStopper); ok {
				ssj.Stop(ctx)
			}
		}
		mgr.mgrMx.Unlock()
		return err
	}
	mgr.idxAr.Reset() // Ensure an initial index run
	mgr.done = make(chan struct{})
	go mgr.notifier()
	go mgr.idxIndexer()

	// mgr.startIndexer(mgr)
	mgr.started = true
	mgr.mgrMx.Unlock()
	mgr.infos <- place.UpdateInfo{Reason: place.OnReload, Zid: id.Invalid}
	return nil
}

// Stop the started place. Now only the Start() function is allowed.
func (mgr *Manager) Stop(ctx context.Context) error {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if !mgr.started {
		return place.ErrStopped
	}
	close(mgr.done)
	var err error
	for _, p := range mgr.subplaces {
		if ss, ok := p.(place.StartStopper); ok {
			if err1 := ss.Stop(ctx); err1 != nil && err == nil {
				err = err1
			}
		}
	}
	mgr.started = false
	return err
}

// ReadStats populates st with place statistics
func (mgr *Manager) ReadStats(st *place.Stats) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	subStats := make([]place.ManagedPlaceStats, len(mgr.subplaces))
	for i, p := range mgr.subplaces {
		p.ReadStats(&subStats[i])
	}

	st.ReadOnly = true
	sumZettel := 0
	for _, sst := range subStats {
		if !sst.ReadOnly {
			st.ReadOnly = false
		}
		sumZettel += sst.Zettel
	}
	st.NumManagedPlaces = len(mgr.subplaces)
	st.ZettelTotal = sumZettel

	var storeSt store.Stats
	mgr.idxMx.RLock()
	defer mgr.idxMx.RUnlock()
	mgr.idxStore.ReadStats(&storeSt)

	st.LastReload = mgr.idxLastReload
	st.IndexesSinceReload = mgr.idxSinceReload
	st.DurLastIndex = mgr.idxDurLastIndex
	st.ZettelIndexed = storeSt.Zettel
	st.IndexUpdates = storeSt.Updates
	st.IndexedWords = storeSt.Words
	st.IndexedUrls = storeSt.Urls
}

// Dump internal data structures to a Writer.
func (mgr *Manager) Dump(w io.Writer) {
	mgr.idxStore.Dump(w)
}

Deleted place/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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place/manager/store"
)

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 zi.meta == nil || 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(ctx context.Context, m *meta.Meta) {
	if ms.doEnrich(ctx, m) {
		ms.mx.Lock()
		ms.updates++
		ms.mx.Unlock()
	}
}

func (ms *memStore) doEnrich(ctx context.Context, 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(meta.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Copy())
	if len(zi.backward) > 0 {
		m.Set(meta.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(meta.KeyForward, zi.forward.String())
		back = remRefs(back, zi.forward)
		updated = true
	}
	if len(zi.meta) > 0 {
		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(meta.KeyBack, back.String())
		updated = true
	}
	return updated
}

// SelectEqual all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SelectEqual(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
}

// Select 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) SelectPrefix(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
	}
	minZid, err := id.Parse(prefix + "00000000000000"[:14-l])
	if err != nil {
		return result
	}
	maxZid, err := id.Parse(prefix + "99999999999999"[:14-l])
	if err != nil {
		return result
	}
	for zid, zi := range ms.idx {
		if minZid <= zid && zid <= maxZid {
			addBackwardZids(result, zid, zi)
		}
	}
	return result
}

// Select 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) SelectSuffix(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
}

// Select all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SelectContains(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] = true
	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(false) {
		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(ctx 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(ctx 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[ref] = true
		}
	}
	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)
	slice := make([]string, 0, len(srefs))
	for s := range srefs {
		slice = append(slice, s)
	}
	sort.Strings(slice)
	for _, s := range slice {
		fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString)
		fmt.Fprintln(w, ":", srefs[s])
	}
}

Deleted place/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
101





































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package memstore stored the index in main memory.
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 place/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






































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package memstore stored the index in main memory.
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) {
	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) {
	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) {
	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) {
	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)
	}
}

Deleted place/manager/place.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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



















































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various places and indexes of a Zettelstore.
package manager

import (
	"context"
	"errors"
	"sort"
	"strings"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
)

// Conatains all place.Place related functions

// Location returns some information where the place is located.
func (mgr *Manager) Location() string {
	if len(mgr.subplaces) <= 2 {
		return "NONE"
	}
	var sb strings.Builder
	for i := 0; i < len(mgr.subplaces)-2; i++ {
		if i > 0 {
			sb.WriteString(", ")
		}
		sb.WriteString(mgr.subplaces[i].Location())
	}
	return sb.String()
}

// CanCreateZettel returns true, if place could possibly create a new zettel.
func (mgr *Manager) CanCreateZettel(ctx context.Context) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.started && mgr.subplaces[0].CanCreateZettel(ctx)
}

// CreateZettel creates a new zettel.
func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return id.Invalid, place.ErrStopped
	}
	return mgr.subplaces[0].CreateZettel(ctx, zettel)
}

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return domain.Zettel{}, place.ErrStopped
	}
	for _, p := range mgr.subplaces {
		if z, err := p.GetZettel(ctx, zid); err != place.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, z.Meta)
			}
			return z, err
		}
	}
	return domain.Zettel{}, place.ErrNotFound
}

// GetMeta retrieves just the meta data of a specific zettel.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, place.ErrStopped
	}
	for _, p := range mgr.subplaces {
		if m, err := p.GetMeta(ctx, zid); err != place.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, m)
			}
			return m, err
		}
	}
	return nil, place.ErrNotFound
}

// FetchZids returns the set of all zettel identifer managed by the place.
func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, place.ErrStopped
	}
	for _, p := range mgr.subplaces {
		zids, err := p.FetchZids(ctx)
		if err != nil {
			return nil, err
		}
		if result == nil {
			result = zids
		} else if len(result) <= len(zids) {
			for zid := range result {
				zids[zid] = true
			}
			result = zids
		} else {
			for zid := range zids {
				result[zid] = true
			}
		}
	}
	return result, 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, s *search.Search) ([]*meta.Meta, error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, place.ErrStopped
	}
	var result []*meta.Meta
	match := s.CompileMatch(mgr)
	for _, p := range mgr.subplaces {
		selected, err := p.SelectMeta(ctx, match)
		if err != nil {
			return nil, err
		}
		sort.Slice(selected, func(i, j int) bool { return selected[i].Zid > selected[j].Zid })
		if len(result) == 0 {
			result = selected
		} else {
			result = place.MergeSorted(result, selected)
		}
	}
	if s == nil {
		return result, nil
	}
	return s.Sort(result), nil
}

// CanUpdateZettel returns true, if place could possibly update the given zettel.
func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.started && mgr.subplaces[0].CanUpdateZettel(ctx, zettel)
}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return place.ErrStopped
	}
	// Remove all (computed) properties from metadata before storing the zettel.
	zettel.Meta = zettel.Meta.Clone()
	for _, p := range zettel.Meta.PairsRest(true) {
		if mgr.propertyKeys[p.Key] {
			zettel.Meta.Delete(p.Key)
		}
	}
	return mgr.subplaces[0].UpdateZettel(ctx, zettel)
}

// AllowRenameZettel returns true, if place will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return false
	}
	for _, p := range mgr.subplaces {
		if !p.AllowRenameZettel(ctx, zid) {
			return false
		}
	}
	return true
}

// RenameZettel changes the current zid to a new zid.
func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return place.ErrStopped
	}
	for i, p := range mgr.subplaces {
		err := p.RenameZettel(ctx, curZid, newZid)
		if err != nil && !errors.Is(err, place.ErrNotFound) {
			for j := 0; j < i; j++ {
				mgr.subplaces[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}
	return nil
}

// CanDeleteZettel returns true, if place could possibly delete the given zettel.
func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return false
	}
	for _, p := range mgr.subplaces {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

// DeleteZettel removes the zettel from the place.
func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return place.ErrStopped
	}
	for _, p := range mgr.subplaces {
		err := p.DeleteZettel(ctx, zid)
		if err == nil {
			return nil
		}
		if !errors.Is(err, place.ErrNotFound) && !errors.Is(err, place.ErrReadOnly) {
			return err
		}
	}
	return place.ErrNotFound
}

Deleted place/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
28
29
30
31
32
33
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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package store contains general index data for storing a zettel index.
package store

import (
	"context"
	"io"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
)

// Stats records statistics about the store.
type Stats struct {
	// Zettel is the number of zettel managed by the indexer.
	Zettel int

	// Updates count the number of metadata updates.
	Updates uint64

	// Words count the different words stored in the store.
	Words uint64

	// Urls count the different URLs stored in the store.
	Urls uint64
}

// Store all relevant zettel data. There may be multiple implementations, i.e.
// memory-based, file-based, based on SQLite, ...
type Store interface {
	place.Enricher
	search.Selector

	// UpdateReferences for a specific zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	UpdateReferences(context.Context, *ZettelIndex) id.Set

	// DeleteZettel removes index data for given zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	DeleteZettel(context.Context, id.Zid) id.Set

	// ReadStats populates st with store statistics.
	ReadStats(st *Stats)

	// Dump the content to a Writer.
	Dump(io.Writer)
}

Deleted place/manager/store/wordset.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package store contains general index data for storing a zettel index.
package store

// WordSet contains the set of all words, with the count of their occurrences.
type WordSet map[string]int

// NewWordSet returns a new WordSet.
func NewWordSet() WordSet { return make(WordSet) }

// Add one word to the set
func (ws WordSet) Add(s string) {
	ws[s] = ws[s] + 1
}

// Words gives the slice of all words in the set.
func (ws WordSet) Words() []string {
	if len(ws) == 0 {
		return nil
	}
	words := make([]string, 0, len(ws))
	for w := range ws {
		words = append(words, w)
	}
	return words
}

// Diff calculates the word slice to be added and to be removed from oldWords
// to get the given word set.
func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) {
	if len(ws) == 0 {
		return nil, oldWords
	}
	if len(oldWords) == 0 {
		return ws.Words(), nil
	}
	oldSet := make(WordSet, len(oldWords))
	for _, ow := range oldWords {
		if _, ok := ws[ow]; ok {
			oldSet[ow] = 1
			continue
		}
		removeWords = append(removeWords, ow)
	}
	for w := range ws {
		if _, ok := oldSet[w]; ok {
			continue
		}
		newWords = append(newWords, w)
	}
	return newWords, removeWords
}

Deleted place/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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76












































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package store contains general index data for storing a zettel index.
package store_test

import (
	"sort"
	"testing"

	"zettelstore.de/z/place/manager/store"
)

func equalWordList(exp, got []string) bool {
	if len(exp) != len(got) {
		return false
	}
	if len(got) == 0 {
		return len(exp) == 0
	}
	sort.Strings(got)
	for i, w := range exp {
		if w != got[i] {
			return false
		}
	}
	return true
}

func TestWordsWords(t *testing.T) {
	testcases := []struct {
		words store.WordSet
		exp   []string
	}{
		{nil, nil},
		{store.WordSet{}, nil},
		{store.WordSet{"a": 1, "b": 2}, []string{"a", "b"}},
	}
	for i, tc := range testcases {
		got := tc.words.Words()
		if !equalWordList(tc.exp, got) {
			t.Errorf("%d: %v.Words() == %v, but got %v", i, tc.words, tc.exp, got)
		}
	}
}

func TestWordsDiff(t *testing.T) {
	testcases := []struct {
		cur        store.WordSet
		old        []string
		expN, expR []string
	}{
		{nil, nil, nil, nil},
		{store.WordSet{}, []string{}, nil, nil},
		{store.WordSet{"a": 1}, []string{}, []string{"a"}, nil},
		{store.WordSet{"a": 1}, []string{"b"}, []string{"a"}, []string{"b"}},
		{store.WordSet{}, []string{"b"}, nil, []string{"b"}},
		{store.WordSet{"a": 1}, []string{"a"}, nil, nil},
	}
	for i, tc := range testcases {
		gotN, gotR := tc.cur.Diff(tc.old)
		if !equalWordList(tc.expN, gotN) {
			t.Errorf("%d: %v.Diff(%v)->new %v, but got %v", i, tc.cur, tc.old, tc.expN, gotN)
		}
		if !equalWordList(tc.expR, gotR) {
			t.Errorf("%d: %v.Diff(%v)->rem %v, but got %v", i, tc.cur, tc.old, tc.expR, gotR)
		}
	}
}

Deleted place/manager/store/zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

























































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package store contains general index data for storing a zettel index.
package store

import "zettelstore.de/z/domain/id"

// ZettelIndex contains all index data of a zettel.
type ZettelIndex struct {
	Zid      id.Zid            // zid of the indexed zettel
	backrefs id.Set            // set of back references
	metarefs map[string]id.Set // references to inverse keys
	deadrefs id.Set            // set of dead references
	words    WordSet
	urls     WordSet
}

// NewZettelIndex creates a new zettel index.
func NewZettelIndex(zid id.Zid) *ZettelIndex {
	return &ZettelIndex{
		Zid:      zid,
		backrefs: id.NewSet(),
		metarefs: make(map[string]id.Set),
		deadrefs: id.NewSet(),
	}
}

// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
	zi.backrefs[zid] = true
}

// AddMetaRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) {
	if zids, ok := zi.metarefs[key]; ok {
		zids[zid] = true
		return
	}
	zi.metarefs[key] = id.NewSet(zid)
}

// AddDeadRef adds a dead reference to a zettel.
func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
	zi.deadrefs[zid] = true
}

// SetWords sets the words to the given value.
func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words }

// SetUrls sets the words to the given value.
func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls }

// GetDeadRefs returns all dead references as a sorted list.
func (zi *ZettelIndex) GetDeadRefs() id.Slice {
	return zi.deadrefs.Sorted()
}

// GetBackRefs returns all back references as a sorted list.
func (zi *ZettelIndex) GetBackRefs() id.Slice {
	return zi.backrefs.Sorted()
}

// GetMetaRefs returns all meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice {
	if len(zi.metarefs) == 0 {
		return nil
	}
	result := make(map[string]id.Slice, len(zi.metarefs))
	for key, refs := range zi.metarefs {
		result[key] = refs.Sorted()
	}
	return result
}

// GetWords returns a reference to the set of words. It must not be modified.
func (zi *ZettelIndex) GetWords() WordSet { return zi.words }

// GetUrls returns a reference to the set of URLs. It must not be modified.
func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls }

Deleted place/memplace/memplace.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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








































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package memplace stores zettel volatile in main memory.
package memplace

import (
	"context"
	"net/url"
	"sync"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"
	"zettelstore.de/z/search"
)

func init() {
	manager.Register(
		"mem",
		func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) {
			return &memPlace{u: u, cdata: *cdata}, nil
		})
}

type memPlace struct {
	u      *url.URL
	cdata  manager.ConnectData
	zettel map[id.Zid]domain.Zettel
	mx     sync.RWMutex
}

func (mp *memPlace) notifyChanged(reason place.UpdateReason, zid id.Zid) {
	if chci := mp.cdata.Notify; chci != nil {
		chci <- place.UpdateInfo{Reason: reason, Zid: zid}
	}
}

func (mp *memPlace) Location() string {
	return mp.u.String()
}

func (mp *memPlace) Start(ctx context.Context) error {
	mp.mx.Lock()
	mp.zettel = make(map[id.Zid]domain.Zettel)
	mp.mx.Unlock()
	return nil
}

func (mp *memPlace) Stop(ctx context.Context) error {
	mp.mx.Lock()
	mp.zettel = nil
	mp.mx.Unlock()
	return nil
}

func (mp *memPlace) CanCreateZettel(ctx context.Context) bool { return true }

func (mp *memPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	mp.mx.Lock()
	zid, err := place.GetNewZid(func(zid id.Zid) (bool, error) {
		_, ok := mp.zettel[zid]
		return !ok, nil
	})
	if err != nil {
		mp.mx.Unlock()
		return id.Invalid, err
	}
	meta := zettel.Meta.Clone()
	meta.Zid = zid
	zettel.Meta = meta
	mp.zettel[zid] = zettel
	mp.mx.Unlock()
	mp.notifyChanged(place.OnUpdate, zid)
	return zid, nil
}

func (mp *memPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	mp.mx.RLock()
	zettel, ok := mp.zettel[zid]
	mp.mx.RUnlock()
	if !ok {
		return domain.Zettel{}, place.ErrNotFound
	}
	zettel.Meta = zettel.Meta.Clone()
	return zettel, nil
}

func (mp *memPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mp.mx.RLock()
	zettel, ok := mp.zettel[zid]
	mp.mx.RUnlock()
	if !ok {
		return nil, place.ErrNotFound
	}
	return zettel.Meta.Clone(), nil
}

func (mp *memPlace) FetchZids(ctx context.Context) (id.Set, error) {
	mp.mx.RLock()
	result := id.NewSetCap(len(mp.zettel))
	for zid := range mp.zettel {
		result[zid] = true
	}
	mp.mx.RUnlock()
	return result, nil
}

func (mp *memPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) {
	result := make([]*meta.Meta, 0, len(mp.zettel))
	mp.mx.RLock()
	for _, zettel := range mp.zettel {
		m := zettel.Meta.Clone()
		mp.cdata.Enricher.Enrich(ctx, m)
		if match(m) {
			result = append(result, m)
		}
	}
	mp.mx.RUnlock()
	return result, nil
}

func (mp *memPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	return true
}

func (mp *memPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	mp.mx.Lock()
	meta := zettel.Meta.Clone()
	if !meta.Zid.IsValid() {
		return &place.ErrInvalidID{Zid: meta.Zid}
	}
	zettel.Meta = meta
	mp.zettel[meta.Zid] = zettel
	mp.mx.Unlock()
	mp.notifyChanged(place.OnUpdate, meta.Zid)
	return nil
}

func (mp *memPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true }

func (mp *memPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mp.mx.Lock()
	zettel, ok := mp.zettel[curZid]
	if !ok {
		mp.mx.Unlock()
		return place.ErrNotFound
	}

	// Check that there is no zettel with newZid
	if _, ok = mp.zettel[newZid]; ok {
		mp.mx.Unlock()
		return &place.ErrInvalidID{Zid: newZid}
	}

	meta := zettel.Meta.Clone()
	meta.Zid = newZid
	zettel.Meta = meta
	mp.zettel[newZid] = zettel
	delete(mp.zettel, curZid)
	mp.mx.Unlock()
	mp.notifyChanged(place.OnDelete, curZid)
	mp.notifyChanged(place.OnUpdate, newZid)
	return nil
}

func (mp *memPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	mp.mx.RLock()
	_, ok := mp.zettel[zid]
	mp.mx.RUnlock()
	return ok
}

func (mp *memPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mp.mx.Lock()
	if _, ok := mp.zettel[zid]; !ok {
		mp.mx.Unlock()
		return place.ErrNotFound
	}
	delete(mp.zettel, zid)
	mp.mx.Unlock()
	mp.notifyChanged(place.OnDelete, zid)
	return nil
}

func (mp *memPlace) ReadStats(st *place.ManagedPlaceStats) {
	st.ReadOnly = false
	mp.mx.RLock()
	st.Zettel = len(mp.zettel)
	mp.mx.RUnlock()
}

Deleted place/merge.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46














































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package place provides a generic interface to zettel places.
package place

import "zettelstore.de/z/domain/meta"

// MergeSorted returns a merged sequence of metadata, sorted by Zid.
// The lists first and second must be sorted descending by Zid.
func MergeSorted(first, second []*meta.Meta) []*meta.Meta {
	lenFirst := len(first)
	lenSecond := len(second)
	result := make([]*meta.Meta, 0, lenFirst+lenSecond)
	iFirst := 0
	iSecond := 0
	for iFirst < lenFirst && iSecond < lenSecond {
		zidFirst := first[iFirst].Zid
		zidSecond := second[iSecond].Zid
		if zidFirst > zidSecond {
			result = append(result, first[iFirst])
			iFirst++
		} else if zidFirst < zidSecond {
			result = append(result, second[iSecond])
			iSecond++
		} else { // zidFirst == zidSecond
			result = append(result, first[iFirst])
			iFirst++
			iSecond++
		}
	}
	if iFirst < lenFirst {
		result = append(result, first[iFirst:]...)
	} else {
		result = append(result, second[iSecond:]...)
	}

	return result
}

Deleted place/place.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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













































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package place provides a generic interface to zettel places.
package place

import (
	"context"
	"errors"
	"fmt"
	"io"
	"time"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
)

// BasePlace is implemented by all Zettel places.
type BasePlace interface {
	// Location returns some information where the place is located.
	// Format is dependent of the place.
	Location() string

	// CanCreateZettel returns true, if place could possibly create a new zettel.
	CanCreateZettel(ctx context.Context) bool

	// CreateZettel creates a new zettel.
	// Returns the new zettel id (and an error indication).
	CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error)

	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)

	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)

	// FetchZids returns the set of all zettel identifer managed by the place.
	FetchZids(ctx context.Context) (id.Set, error)

	// CanUpdateZettel returns true, if place 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 place 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 place could possibly delete the given zettel.
	CanDeleteZettel(ctx context.Context, zid id.Zid) bool

	// DeleteZettel removes the zettel from the place.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

// ManagedPlace is the interface of managed places.
type ManagedPlace interface {
	BasePlace

	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error)

	// ReadStats populates st with place statistics
	ReadStats(st *ManagedPlaceStats)
}

// ManagedPlaceStats records statistics about the place.
type ManagedPlaceStats struct {
	// ReadOnly indicates that the places cannot be changed
	ReadOnly bool

	// Zettel is the number of zettel managed by the place.
	Zettel int
}

// StartStopper performs simple lifecycle management.
type StartStopper interface {
	// Start the place. Now all other functions of the place are allowed.
	// Starting an already started place is not allowed.
	Start(ctx context.Context) error

	// Stop the started place. Now only the Start() function is allowed.
	Stop(ctx context.Context) error
}

// Place is a place to be used outside the place package and its descendants.
type Place interface {
	BasePlace

	// SelectMeta returns a list of metadata that comply to the given selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)

	// ReadStats populates st with place statistics
	ReadStats(st *Stats)

	// Dump internal data to a Writer.
	Dump(w io.Writer)
}

// Stats record stattistics about a full place.
type Stats struct {
	// ReadOnly indicates that the places cannot be changed
	ReadOnly bool

	// NumManagedPlaces is the number of places managed.
	NumManagedPlaces int

	// Zettel is the number of zettel managed by the place, including
	// duplicates across managed places.
	ZettelTotal int

	// LastReload stores the timestamp when a full re-index was done.
	LastReload time.Time

	// IndexesSinceReload counts indexing a zettel since the full re-index.
	IndexesSinceReload uint64

	// DurLastIndex is the duration of the last index run. This could be a
	// full re-index or a re-index of a single zettel.
	DurLastIndex time.Duration

	// ZettelIndexed is the number of zettel managed by the indexer.
	ZettelIndexed int

	// IndexUpdates count the number of metadata updates.
	IndexUpdates uint64

	// IndexedWords count the different words indexed.
	IndexedWords uint64

	// IndexedUrls count the different URLs indexed.
	IndexedUrls uint64
}

// Manager is a place-managing place.
type Manager interface {
	Place
	StartStopper
	Subject
}

// UpdateReason gives an indication, why the ObserverFunc was called.
type UpdateReason uint8

// Values for Reason
const (
	_        UpdateReason = iota
	OnReload              // Place was reloaded
	OnUpdate              // A zettel was created or changed
	OnDelete              // A zettel was removed
)

// UpdateInfo contains all the data about a changed zettel.
type UpdateInfo struct {
	Place  Place
	Reason UpdateReason
	Zid    id.Zid
}

// UpdateFunc is a function to be called when a change is detected.
type UpdateFunc func(UpdateInfo)

// Subject is a place 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)
}

// Enricher is used to update metadata by adding new properties.
type Enricher interface {
	// Enrich computes additional properties and updates the given metadata.
	// It is typically called by zettel reading methods.
	Enrich(ctx context.Context, m *meta.Meta)
}

// NoEnrichContext will signal an enricher that nothing has to be done.
// This is useful for an Indexer, but also for some place.Place calls, when
// just the plain metadata is needed.
func NoEnrichContext(ctx context.Context) context.Context {
	return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
}

type ctxNoEnrichType struct{}

var ctxNoEnrichKey ctxNoEnrichType

// DoNotEnrich determines if the context is marked to not enrich metadata.
func DoNotEnrich(ctx context.Context) bool {
	_, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType)
	return ok
}

// ErrNotAllowed is returned if the caller is not allowed to perform the operation.
type ErrNotAllowed struct {
	Op   string
	User *meta.Meta
	Zid  id.Zid
}

// NewErrNotAllowed creates an new authorization error.
func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error {
	return &ErrNotAllowed{
		Op:   op,
		User: user,
		Zid:  zid,
	}
}

func (err *ErrNotAllowed) Error() string {
	if err.User == nil {
		if err.Zid.IsValid() {
			return fmt.Sprintf(
				"operation %q on zettel %v not allowed for not authorized user",
				err.Op,
				err.Zid.String())
		}
		return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op)
	}
	if err.Zid.IsValid() {
		return fmt.Sprintf(
			"operation %q on zettel %v not allowed for user %v/%v",
			err.Op,
			err.Zid.String(),
			err.User.GetDefault(meta.KeyUserID, "?"),
			err.User.Zid.String())
	}
	return fmt.Sprintf(
		"operation %q not allowed for user %v/%v",
		err.Op,
		err.User.GetDefault(meta.KeyUserID, "?"),
		err.User.Zid.String())
}

// Is return true, if the error is of type ErrNotAllowed.
func (err *ErrNotAllowed) Is(target error) bool { return true }

// ErrStarted is returned when trying to start an already started place.
var ErrStarted = errors.New("place is already started")

// ErrStopped is returned if calling methods on a place that was not started.
var ErrStopped = errors.New("place is stopped")

// ErrReadOnly is returned if there is an attepmt to write to a read-only place.
var ErrReadOnly = errors.New("read-only place")

// ErrNotFound is returned if a zettel was not found in the place.
var ErrNotFound = errors.New("zettel not found")

// ErrConflict is returned if a place operation detected a conflict..
// One example: if calculating a new zettel identifier takes too long.
var ErrConflict = errors.New("conflict")

// ErrInvalidID is returned if the zettel id is not appropriate for the place operation.
type ErrInvalidID struct{ Zid id.Zid }

func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() }

Deleted place/progplace/config.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52




















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package progplace provides zettel that inform the user about the internal Zettelstore state.
package progplace

import (
	"strings"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

func genConfigZettelM(zid id.Zid) *meta.Meta {
	if myConfig == nil {
		return nil
	}
	m := meta.New(zid)
	m.Set(meta.KeyTitle, "Zettelstore Startup Configuration")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
	return m
}

func genConfigZettelC(m *meta.Meta) string {
	var sb strings.Builder
	for i, p := range myConfig.Pairs(false) {
		if i > 0 {
			sb.WriteByte('\n')
		}
		sb.WriteString("; ''")
		sb.WriteString(p.Key)
		sb.WriteString("''")
		if p.Value != "" {
			sb.WriteString("\n: ``")
			for _, r := range p.Value {
				if r == '`' {
					sb.WriteByte('\\')
				}
				sb.WriteRune(r)
			}
			sb.WriteString("``")
		}
	}
	return sb.String()
}

Deleted place/progplace/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






































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package progplace provides zettel that inform the user about the internal Zettelstore state.
package progplace

import (
	"fmt"
	"strings"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

func genKeysM(zid id.Zid) *meta.Meta {
	m := meta.New(zid)
	m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin)
	return m
}

func genKeysC(*meta.Meta) string {
	keys := meta.GetSortedKeyDescriptions()
	var sb strings.Builder
	sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n")
	for _, kd := range keys {
		fmt.Fprintf(&sb,
			"|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty())
	}
	return sb.String()
}

Deleted place/progplace/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









































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package progplace provides zettel that inform the user about the internal
// Zettelstore state.
package progplace

import (
	"fmt"
	"strings"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

func genManagerM(zid id.Zid) *meta.Meta {
	m := meta.New(zid)
	m.Set(meta.KeyTitle, "Zettelstore Place Manager")
	return m
}

func genManagerC(*meta.Meta) string {
	kvl := kernel.Main.GetServiceStatistics(kernel.PlaceService)
	if len(kvl) == 0 {
		return "No statistics available"
	}
	var sb strings.Builder
	sb.WriteString("|=Name|=Value>\n")
	for _, kv := range kvl {
		fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value)
	}
	return sb.String()
}

Deleted place/progplace/progplace.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package progplace provides zettel that inform the user about the internal
// Zettelstore state.
package progplace

import (
	"context"
	"net/url"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"
	"zettelstore.de/z/search"
)

func init() {
	manager.Register(
		" prog",
		func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) {
			return getPlace(cdata.Enricher), nil
		})
}

type progPlace struct {
	filter place.Enricher
}

var myConfig *meta.Meta
var myZettel = map[id.Zid]struct {
	meta    func(id.Zid) *meta.Meta
	content func(*meta.Meta) string
}{
	id.VersionZid:              {genVersionBuildM, genVersionBuildC},
	id.HostZid:                 {genVersionHostM, genVersionHostC},
	id.OperatingSystemZid:      {genVersionOSM, genVersionOSC},
	id.PlaceManagerZid:         {genManagerM, genManagerC},
	id.MetadataKeyZid:          {genKeysM, genKeysC},
	id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC},
}

// Get returns the one program place.
func getPlace(mf place.Enricher) place.ManagedPlace {
	return &progPlace{filter: mf}
}

// Setup remembers important values.
func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() }

func (pp *progPlace) Location() string { return "" }

func (pp *progPlace) CanCreateZettel(ctx context.Context) bool { return false }

func (pp *progPlace) CreateZettel(
	ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	return id.Invalid, place.ErrReadOnly
}

func (pp *progPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	if gen, ok := myZettel[zid]; ok && gen.meta != nil {
		if m := gen.meta(zid); m != nil {
			updateMeta(m)
			if genContent := gen.content; genContent != nil {
				return domain.Zettel{
					Meta:    m,
					Content: domain.NewContent(genContent(m)),
				}, nil
			}
			return domain.Zettel{Meta: m}, nil
		}
	}
	return domain.Zettel{}, place.ErrNotFound
}

func (pp *progPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	if gen, ok := myZettel[zid]; ok {
		if genMeta := gen.meta; genMeta != nil {
			if m := genMeta(zid); m != nil {
				updateMeta(m)
				return m, nil
			}
		}
	}
	return nil, place.ErrNotFound
}

func (pp *progPlace) FetchZids(ctx context.Context) (id.Set, error) {
	result := id.NewSetCap(len(myZettel))
	for zid, gen := range myZettel {
		if genMeta := gen.meta; genMeta != nil {
			if genMeta(zid) != nil {
				result[zid] = true
			}
		}
	}
	return result, nil
}

func (pp *progPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
	for zid, gen := range myZettel {
		if genMeta := gen.meta; genMeta != nil {
			if m := genMeta(zid); m != nil {
				updateMeta(m)
				pp.filter.Enrich(ctx, m)
				if match(m) {
					res = append(res, m)
				}
			}
		}
	}
	return res, nil
}

func (pp *progPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	return false
}

func (pp *progPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	return place.ErrReadOnly
}

func (pp *progPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	_, ok := myZettel[zid]
	return !ok
}

func (pp *progPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if _, ok := myZettel[curZid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}

func (pp *progPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false }

func (pp *progPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := myZettel[zid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}

func (pp *progPlace) ReadStats(st *place.ManagedPlaceStats) {
	st.ReadOnly = true
	st.Zettel = len(myZettel)
}

func updateMeta(m *meta.Meta) {
	m.Set(meta.KeyNoIndex, meta.ValueTrue)
	m.Set(meta.KeySyntax, meta.ValueSyntaxZmk)
	m.Set(meta.KeyRole, meta.ValueRoleConfiguration)
	m.Set(meta.KeyLang, meta.ValueLangEN)
	m.Set(meta.KeyReadOnly, meta.ValueTrue)
	if _, ok := m.Get(meta.KeyVisibility); !ok {
		m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
	}
}

Deleted place/progplace/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






















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package progplace provides zettel that inform the user about the internal Zettelstore state.
package progplace

import (
	"fmt"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

func getVersionMeta(zid id.Zid, title string) *meta.Meta {
	m := meta.New(zid)
	m.Set(meta.KeyTitle, title)
	m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
	return m
}

func genVersionBuildM(zid id.Zid) *meta.Meta {
	m := getVersionMeta(zid, "Zettelstore Version")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
	return m
}
func genVersionBuildC(*meta.Meta) string {
	return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)
}

func genVersionHostM(zid id.Zid) *meta.Meta {
	return getVersionMeta(zid, "Zettelstore Host")
}
func genVersionHostC(*meta.Meta) string {
	return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)
}

func genVersionOSM(zid id.Zid) *meta.Meta {
	return getVersionMeta(zid, "Zettelstore Operating System")
}
func genVersionOSC(*meta.Meta) string {
	return fmt.Sprintf(
		"%v/%v",
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string),
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string),
	)
}

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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"sort"

	"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
}

// 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)
	result = c.sortElements(result)
	result = c.offsetElements(result)
	return limitElements(result, c.limit)
}

// 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 {
		return sortMetaByZid(metaList)
	}

	if c.isDeterministic() {
		// We need to sort to make it deterministic
		if len(c.order) == 0 || c.order[0].isRandom() {
			metaList = sortMetaByZid(metaList)
		} else {
			sort.Slice(metaList, createSortFunc(c.order, metaList))
		}
	}
	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 {
			sort.Slice(metaList, createSortFunc(c.order, metaList))
		}
	}
	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
	sort.Ints(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
}

func sortMetaByZid(metaList []*meta.Meta) []*meta.Meta {
	sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
	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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ContextSpec contains all specification values for calculating a context.
type ContextSpec struct {
	Direction ContextDirection
	MaxCost   int
	MaxCount  int
	Full      bool
}

// ContextDirection specifies the direction a context should be calculated.
type ContextDirection uint8

const (
	ContextDirBoth ContextDirection = iota
	ContextDirForward
	ContextDirBackward
)

// ContextPort is the collection of box methods needed by this directive.
type ContextPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error)
}

func (spec *ContextSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ContextDirective)
	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)
}

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 1
	case api.KeySubordinates, api.KeySuperior:
		return 1.5
	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() && (len(ct.seen) <= 1 || ct.maxCost == 0 || newCost <= ct.maxCost) {
		if _, found := ct.seen[m.Zid]; !found {
			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.Copy(zs)
	}
	for _, zid := range zidSet.Sorted() { // .Sorted() to stay deterministic
		minCost := math.MaxFloat64
		costFactor := 1.1
		for _, tag := range tags {
			tagZids := ct.tagZids[tag]
			if tagZids.Contains(zid) {
				cost := tagCost(baseCost, len(tagZids))
				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 _, found := ct.seen[zid]; found {
			continue
		}
		ct.seen.Add(zid)
		return m, item.cost
	}
	return nil, -1
}

func (ct *contextTask) hasLimit() bool {
	limit := ct.limit
	return limit > 0 && len(ct.seen) >= limit
}

Added 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/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 {
	state := parserState{
		inp: input.NewInput([]byte(spec)),
	}
	q = state.parse(q)
	if q != nil {
		for len(q.terms) > 1 && q.terms[len(q.terms)-1].isEmpty() {
			q.terms = q.terms[:len(q.terms)-1]
		}
	}
	return q
}

type parserState struct {
	inp *input.Input
}

func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS }
func (ps *parserState) acceptSingleKw(s string) bool {
	if ps.inp.Accept(s) && (ps.isSpace() || ps.isActionSep() || ps.mustStop()) {
		return true
	}
	return false
}
func (ps *parserState) acceptKwArgs(s string) bool {
	if ps.inp.Accept(s) && ps.isSpace() {
		ps.skipSpace()
		return true
	}
	return false
}

const (
	actionSeparatorChar       = '|'
	existOperatorChar         = '?'
	searchOperatorNotChar     = '!'
	searchOperatorEqualChar   = '='
	searchOperatorHasChar     = ':'
	searchOperatorPrefixChar  = '['
	searchOperatorSuffixChar  = ']'
	searchOperatorMatchChar   = '~'
	searchOperatorLessChar    = '<'
	searchOperatorGreaterChar = '>'
)

func (ps *parserState) parse(q *Query) *Query {
	ps.skipSpace()
	if ps.mustStop() {
		return q
	}
	inp := ps.inp
	firstPos := inp.Pos
	zidSet := id.NewSet()
	for {
		pos := inp.Pos
		zid, found := ps.scanZid()
		if !found {
			inp.SetPos(pos)
			break
		}
		if !zidSet.ContainsOrNil(zid) {
			zidSet.Add(zid)
			q = createIfNeeded(q)
			q.zids = append(q.zids, zid)
		}
		ps.skipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
		}
	}

	hasContext := false
	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {
			if hasContext {
				inp.SetPos(pos)
				break
			}
			q = ps.parseContext(q)
			hasContext = true
			continue
		}
		inp.SetPos(pos)
		if q == nil || len(q.zids) == 0 {
			break
		}
		if ps.acceptSingleKw(api.IdentDirective) {
			q.directives = append(q.directives, &IdentSpec{})
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.ItemsDirective) {
			q.directives = append(q.directives, &ItemsSpec{})
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.UnlinkedDirective) {
			q = ps.parseUnlinked(q)
			continue
		}
		inp.SetPos(pos)
		break
	}
	if q != nil && len(q.directives) == 0 {
		inp.SetPos(firstPos) // No directive -> restart at beginning
		q.zids = nil
	}

	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.OrDirective) {
			q = createIfNeeded(q)
			if !q.terms[len(q.terms)-1].isEmpty() {
				q.terms = append(q.terms, conjTerms{})
			}
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.RandomDirective) {
			q = createIfNeeded(q)
			if len(q.order) == 0 {
				q.order = []sortOrder{{"", false}}
			}
			continue
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.PickDirective) {
			if s, ok := ps.parsePick(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.OrderDirective) {
			if s, ok := ps.parseOrder(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.OffsetDirective) {
			if s, ok := ps.parseOffset(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.LimitDirective) {
			if s, ok := ps.parseLimit(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if 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 {
		ps.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 {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptKwArgs(api.PhraseDirective) {
			if word := ps.scanWord(); len(word) > 0 {
				spec.words = append(spec.words, string(word))
				continue
			}
		}

		inp.SetPos(pos)
		break
	}
	q.directives = append(q.directives, spec)
	return q
}

func (ps *parserState) parsePick(q *Query) (*Query, bool) {
	num, ok := ps.scanPosInt()
	if !ok {
		return q, false
	}
	q = createIfNeeded(q)
	if q.pick == 0 || q.pick >= num {
		q.pick = num
	}
	return q, true
}

func (ps *parserState) parseOrder(q *Query) (*Query, bool) {
	reverse := false
	if ps.acceptKwArgs(api.ReverseDirective) {
		reverse = true
	}
	word := ps.scanWord()
	if len(word) == 0 {
		return q, false
	}
	if sWord := string(word); meta.KeyIsValid(sWord) {
		q = createIfNeeded(q)
		if len(q.order) == 1 && q.order[0].isRandom() {
			q.order = nil
		}
		q.order = append(q.order, sortOrder{sWord, reverse})
		return q, true
	}
	return q, false
}

func (ps *parserState) parseOffset(q *Query) (*Query, bool) {
	num, ok := ps.scanPosInt()
	if !ok {
		return q, false
	}
	q = createIfNeeded(q)
	if q.offset <= num {
		q.offset = num
	}
	return q, true
}

func (ps *parserState) parseLimit(q *Query) (*Query, bool) {
	num, ok := ps.scanPosInt()
	if !ok {
		return q, false
	}
	q = createIfNeeded(q)
	if q.limit == 0 || q.limit >= num {
		q.limit = num
	}
	return q, true
}

func (ps *parserState) parseActions(q *Query) *Query {
	ps.inp.Next()
	var words []string
	for {
		ps.skipSpace()
		word := ps.scanWord()
		if len(word) == 0 {
			break
		}
		words = append(words, string(word))
	}
	if len(words) > 0 {
		q = createIfNeeded(q)
		q.actions = words
	}
	return q
}

func (ps *parserState) parseText(q *Query) *Query {
	inp := ps.inp
	pos := inp.Pos
	op, hasOp := ps.scanSearchOp()
	if hasOp && (op == cmpExist || op == cmpNotExist) {
		inp.SetPos(pos)
		hasOp = false
	}
	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() || ps.isActionSep() || ps.mustStop() {
				return q.addKey(string(key), op)
			}
			ps.inp.SetPos(pos)
			hasOp = false
			text = ps.scanWord()
			key = nil
		} else {
			text = ps.scanWord()
		}
	} else if len(text) == 0 {
		// Only an empty search operation is found -> ignore it
		return q
	}
	q = createIfNeeded(q)
	if hasOp {
		if key == nil {
			q.addSearch(expValue{string(text), op})
		} else {
			last := len(q.terms) - 1
			if q.terms[last].mvals == nil {
				q.terms[last].mvals = expMetaValues{string(key): {expValue{string(text), op}}}
			} else {
				sKey := string(key)
				q.terms[last].mvals[sKey] = append(q.terms[last].mvals[sKey], expValue{string(text), op})
			}
		}
	} else {
		// Assert key == nil
		q.addSearch(expValue{string(text), cmpMatch})
	}
	return q
}

func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) {
	inp := ps.inp
	pos := inp.Pos
	allowKey := !hasOp

	for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() {
		if allowKey {
			switch inp.Ch {
			case searchOperatorNotChar, existOperatorChar,
				searchOperatorEqualChar, searchOperatorHasChar,
				searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar,
				searchOperatorLessChar, searchOperatorGreaterChar:
				allowKey = false
				if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) {
					return nil, key
				}
			}
		}
		inp.Next()
	}
	return inp.Src[pos:inp.Pos], nil
}

func (ps *parserState) scanWord() []byte {
	inp := ps.inp
	pos := inp.Pos
	for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() {
		inp.Next()
	}
	return inp.Src[pos:inp.Pos]
}

func (ps *parserState) scanPosInt() (int, bool) {
	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) skipSpace() {
	for ps.isSpace() {
		ps.inp.Next()
	}
}

func (ps *parserState) isSpace() bool {
	switch ch := ps.inp.Ch; ch {
	case input.EOS:
		return false
	case ' ', '\t', '\n', '\r':
		return true
	default:
		return input.IsSpace(ch)
	}
}

func (ps *parserState) isActionSep() bool {
	return ps.inp.Ch == actionSeparatorChar
}

Added 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
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) 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`},
		{`key=`, `key=`}, {`key!=`, `key!=`},
		{`key:`, `key:`}, {`key!:`, `key!:`},
		{`key[`, `key[`}, {`key![`, `key![`},
		{`key]`, `key]`}, {`key!]`, `key!]`},
		{`key~`, `key~`}, {`key!~`, `key!~`},
		{`key<`, `key<`}, {`key!<`, `key!<`},
		{`key>`, `key>`}, {`key!>`, `key!>`},
		{`key=a`, `key=a`}, {`key!=a`, `key!=a`},
		{`key:a`, `key:a`}, {`key!:a`, `key!:a`},
		{`key[a`, `key[a`}, {`key![a`, `key![a`},
		{`key]a`, `key]a`}, {`key!]a`, `key!]a`},
		{`key~a`, `key~a`}, {`key!~a`, `key!~a`},
		{`key<a`, `key<a`}, {`key!<a`, `key!<a`},
		{`key>a`, `key>a`}, {`key!>a`, `key!>a`},
		{`key1:a key2:b`, `key1:a key2:b`},
		{`key1: key2:b`, `key1: key2:b`},
		{"word key:a", "key:a word"},
		{`PICK 3`, `PICK 3`}, {`PICK 9 PICK 11`, `PICK 9`},
		{"PICK a", "PICK a"},
		{`RANDOM`, `RANDOM`}, {`RANDOM a`, `a RANDOM`}, {`a RANDOM`, `a RANDOM`},
		{`RANDOM RANDOM a`, `a RANDOM`},
		{`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", "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", "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()
		if tc.exp != got {
			t.Errorf("%d: Parse(%q) does not yield %q, but got %q", i, tc.spec, tc.exp, got)
			continue
		}

		gotReparse := query.Parse(got).String()
		if gotReparse != got {
			t.Errorf("%d: Parse(%q) does not yield itself, but %q", i, got, gotReparse)
		}

		gotPipe := query.Parse(got + "|").String()
		if gotPipe != got {
			t.Errorf("%d: Parse(%q) does not yield itself, but %q", i, got+"|", gotReparse)
		}
	}
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/zettel/id"
)

var op2string = map[compareOp]string{
	cmpExist:     api.ExistOperator,
	cmpNotExist:  api.ExistNotOperator,
	cmpEqual:     api.SearchOperatorEqual,
	cmpNotEqual:  api.SearchOperatorNotEqual,
	cmpHas:       api.SearchOperatorHas,
	cmpHasNot:    api.SearchOperatorHasNot,
	cmpPrefix:    api.SearchOperatorPrefix,
	cmpNoPrefix:  api.SearchOperatorNoPrefix,
	cmpSuffix:    api.SearchOperatorSuffix,
	cmpNoSuffix:  api.SearchOperatorNoSuffix,
	cmpMatch:     api.SearchOperatorMatch,
	cmpNoMatch:   api.SearchOperatorNoMatch,
	cmpLess:      api.SearchOperatorLess,
	cmpNoLess:    api.SearchOperatorNotLess,
	cmpGreater:   api.SearchOperatorGreater,
	cmpNoGreater: api.SearchOperatorNotGreater,
}

func (q *Query) String() string {
	var sb strings.Builder
	q.Print(&sb)
	return sb.String()
}

// Print the query in a parseable form.
func (q *Query) Print(w io.Writer) {
	if q == nil {
		return
	}
	env := PrintEnv{w: w}
	env.printZids(q.zids)
	for _, d := range q.directives {
		d.Print(&env)
	}
	for i, term := range q.terms {
		if i > 0 {
			env.writeString(" OR")
		}
		for _, name := range maps.Keys(term.keys) {
			env.printSpace()
			env.writeString(name)
			if op := term.keys[name]; op == cmpExist || op == cmpNotExist {
				env.writeString(op2string[op])
			} else {
				env.writeStrings(api.ExistOperator, " ", name, api.ExistNotOperator)
			}
		}
		for _, name := range maps.Keys(term.mvals) {
			env.printExprValues(name, term.mvals[name])
		}
		if len(term.search) > 0 {
			env.printExprValues("", term.search)
		}
	}
	env.printPosInt(api.PickDirective, q.pick)
	env.printOrder(q.order)
	env.printPosInt(api.OffsetDirective, q.offset)
	env.printPosInt(api.LimitDirective, q.limit)
	env.printActions(q.actions)
}

// PrintEnv is an environment where queries are printed.
type PrintEnv struct {
	w     io.Writer
	space bool
}

var bsSpace = []byte{' '}

func (pe *PrintEnv) printSpace() {
	if pe.space {
		pe.w.Write(bsSpace)
		return
	}
	pe.space = true
}
func (pe *PrintEnv) write(ch byte)        { pe.w.Write([]byte{ch}) }
func (pe *PrintEnv) writeString(s string) { io.WriteString(pe.w, s) }
func (pe *PrintEnv) writeStrings(sSeq ...string) {
	for _, s := range sSeq {
		io.WriteString(pe.w, s)
	}
}

func (pe *PrintEnv) printZids(zids []id.Zid) {
	for i, zid := range zids {
		if i > 0 {
			pe.printSpace()
		}
		pe.writeString(zid.String())
		pe.space = true
	}
}
func (pe *PrintEnv) printExprValues(key string, values []expValue) {
	for _, val := range values {
		pe.printSpace()
		pe.writeString(key)
		switch op := val.op; op {
		case cmpMatch:
			// An empty key signals a full-text search. Since "~" is the default op in this case,
			// it can be ignored. Therefore, print only "~" if there is a key.
			if key != "" {
				pe.writeString(api.SearchOperatorMatch)
			}
		case cmpNoMatch:
			// An empty key signals a full-text search. Since "!" is the shortcut for "!~",
			// it can be ignored. Therefore, print only "!~" if there is a key.
			if key == "" {
				pe.writeString(api.SearchOperatorNot)
			} else {
				pe.writeString(api.SearchOperatorNoMatch)
			}
		default:
			if s, found := op2string[op]; found {
				pe.writeString(s)
			} else {
				pe.writeString("%" + strconv.Itoa(int(op)))
			}
		}
		if s := val.value; s != "" {
			pe.writeString(s)
		}
	}
}

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.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 {
				env.writeString(" AND ")
			}
			env.writeString(name)
			switch term.keys[name] {
			case cmpExist:
				env.writeString(" EXIST")
			case cmpNotExist:
				env.writeString(" NOT EXIST")
			default:
				env.writeString(" IS SCHRÖDINGER'S CAT")
			}
			env.space = true
		}
		for _, name := range maps.Keys(term.mvals) {
			if env.space {
				env.writeString(" AND ")
			}
			env.writeString(name)
			env.printHumanSelectExprValues(term.mvals[name])
			env.space = true
		}
		if len(term.search) > 0 {
			if env.space {
				env.writeString(" ")
			}
			env.writeString("ANY")
			env.printHumanSelectExprValues(term.search)
			env.space = true
		}
	}

	env.printPosInt(api.PickDirective, q.pick)
	env.printOrder(q.order)
	env.printPosInt(api.OffsetDirective, q.offset)
	env.printPosInt(api.LimitDirective, q.limit)
	env.printActions(q.actions)
}

func (pe *PrintEnv) printHumanSelectExprValues(values []expValue) {
	if len(values) == 0 {
		pe.writeString(" MATCH ANY")
		return
	}

	for j, val := range values {
		if j > 0 {
			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) {
	for _, o := range order {
		if o.isRandom() {
			pe.printSpace()
			pe.writeString(api.RandomDirective)
			continue
		}
		pe.printSpace()
		pe.writeString(api.OrderDirective)
		if o.descending {
			pe.printSpace()
			pe.writeString(api.ReverseDirective)
		}
		pe.printSpace()
		pe.writeString(o.key)
	}
}

func (pe *PrintEnv) printPosInt(key string, val int) {
	if val > 0 {
		pe.printSpace()
		pe.writeStrings(key, " ", strconv.Itoa(val))
	}
}

func (pe *PrintEnv) printActions(words []string) {
	if len(words) > 0 {
		pe.printSpace()
		pe.write(actionSeparatorChar)
		for _, word := range words {
			pe.printSpace()
			pe.writeString(word)
		}
	}
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/v2"
	"slices"

	"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

	// 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

	// 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

	// Select all zettel that contains the given string.
	// The string must be normalized through Unicode NKFD, trimmed and not empty.
	SearchContains(s string) id.Set
}

// Query specifies a mechanism for querying zettel.
type Query struct {
	// Präfixed zettel identifier.
	zids []id.Zid

	// Querydirectives, like CONTEXT, ...
	directives []Directive

	// Fields to be used for selecting
	preMatch MetaMatchFunc // Match that must be true
	terms    []conjTerms

	// Allow to create predictable randomness
	seed int

	pick int // Randomly pick elements, <= 0: no pick

	// Fields to be used for sorting
	order  []sortOrder
	offset int // <= 0: no offset
	limit  int // <= 0: no limit

	// Execute specification
	actions []string
}

// GetZids returns a slide of all specified zettel identifier.
func (q *Query) GetZids() []id.Zid {
	if q == nil || len(q.zids) == 0 {
		return nil
	}
	result := make([]id.Zid, len(q.zids))
	copy(result, q.zids)
	return result
}

// Directive are executed to process the list of metadata.
type Directive interface {
	Print(*PrintEnv)
}

// GetDirectives returns the slice of query directives.
func (q *Query) GetDirectives() []Directive {
	if q == nil || len(q.directives) == 0 {
		return nil
	}
	result := make([]Directive, len(q.directives))
	copy(result, q.directives)
	return result
}

type keyExistMap map[string]compareOp
type expMetaValues map[string][]expValue

type conjTerms struct {
	keys   keyExistMap
	mvals  expMetaValues // Expected values for a meta datum
	search []expValue    // Search string
}

func (ct *conjTerms) isEmpty() bool {
	return len(ct.keys) == 0 && len(ct.mvals) == 0 && len(ct.search) == 0
}
func (ct *conjTerms) addKey(key string, op compareOp) {
	if ct.keys == nil {
		ct.keys = map[string]compareOp{key: op}
		return
	}
	if prevOp, found := ct.keys[key]; found {
		if prevOp != op {
			ct.keys[key] = cmpUnknown
		}
		return
	}
	ct.keys[key] = op
}
func (ct *conjTerms) addSearch(val expValue) { ct.search = append(ct.search, val) }

type sortOrder struct {
	key        string
	descending bool
}

func (so *sortOrder) isRandom() bool { return so.key == "" }

func createIfNeeded(q *Query) *Query {
	if q == nil {
		return &Query{
			terms: []conjTerms{{}},
		}
	}
	return q
}

// 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,
	cmpEqual:     cmpNotEqual,
	cmpNotEqual:  cmpEqual,
	cmpHas:       cmpHasNot,
	cmpHasNot:    cmpHas,
	cmpPrefix:    cmpNoPrefix,
	cmpNoPrefix:  cmpPrefix,
	cmpSuffix:    cmpNoSuffix,
	cmpNoSuffix:  cmpSuffix,
	cmpMatch:     cmpNoMatch,
	cmpNoMatch:   cmpMatch,
	cmpLess:      cmpNoLess,
	cmpNoLess:    cmpLess,
	cmpGreater:   cmpNoGreater,
	cmpNoGreater: cmpGreater,
}

func (op compareOp) negate() compareOp { return negateMap[op] }

var negativeMap = map[compareOp]bool{
	cmpNotExist:  true,
	cmpNotEqual:  true,
	cmpHasNot:    true,
	cmpNoPrefix:  true,
	cmpNoSuffix:  true,
	cmpNoMatch:   true,
	cmpNoLess:    true,
	cmpNoGreater: true,
}

func (op compareOp) isNegated() bool { return negativeMap[op] }

type expValue struct {
	value string
	op    compareOp
}

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")
	}
	q.preMatch = preMatch
	return q
}

// SetSeed sets a seed value.
func (q *Query) SetSeed(seed int) *Query {
	q = createIfNeeded(q)
	q.seed = seed
	return q
}

// GetSeed returns the seed value if one was set.
func (q *Query) GetSeed() (int, bool) {
	if q == nil {
		return 0, false
	}
	return q.seed, q.seed > 0
}

// SetDeterministic signals that the result should be the same if the seed is the same.
func (q *Query) SetDeterministic() *Query {
	q = createIfNeeded(q)
	if q.seed <= 0 {
		q.seed = int(rand.IntN(10000) + 1)
	}
	return q
}

// Actions returns the slice of action specifications
func (q *Query) Actions() []string {
	if q == nil {
		return nil
	}
	return q.actions
}

// RemoveActions will remove the action part of a query.
func (q *Query) RemoveActions() {
	if q != nil {
		q.actions = nil
	}
}

// EnrichNeeded returns true, if the query references a metadata key that
// is calculated via metadata enrichments.
func (q *Query) EnrichNeeded() bool {
	if q == nil {
		return false
	}
	if len(q.zids) > 0 {
		return true
	}
	if len(q.actions) > 0 {
		// Unknown, what an action will use. Example: RSS needs api.KeyPublished.
		return true
	}
	for _, term := range q.terms {
		for key := range term.keys {
			if meta.IsProperty(key) {
				return true
			}
		}
		for key := range term.mvals {
			if meta.IsProperty(key) {
				return true
			}
		}
	}
	for _, o := range q.order {
		if meta.IsProperty(o.key) {
			return true
		}
	}
	return false
}

// RetrieveAndCompile queries the search index and returns a predicate
// for its results and returns a matching predicate.
func (q *Query) RetrieveAndCompile(_ context.Context, searcher Searcher, metaSeq []*meta.Meta) Compiled {
	if q == nil {
		return Compiled{
			PreMatch: matchAlways,
			Terms: []CompiledTerm{{
				Match:    matchAlways,
				Retrieve: AlwaysIncluded,
			}}}
	}
	q = q.Clone()

	preMatch := q.preMatch
	if preMatch == nil {
		preMatch = matchAlways
	}

	startSet := metaList2idSet(metaSeq)
	result := Compiled{
		hasQuery:  true,
		seed:      q.seed,
		pick:      q.pick,
		order:     q.order,
		offset:    q.offset,
		limit:     q.limit,
		startMeta: metaSeq,
		PreMatch:  preMatch,
		Terms:     []CompiledTerm{},
	}

	for _, term := range q.terms {
		cTerm := term.retrieveAndCompileTerm(searcher, startSet)
		if cTerm.Retrieve == nil {
			if cTerm.Match == nil {
				// no restriction on match/retrieve -> all will match
				result.Terms = []CompiledTerm{{
					Match:    matchAlways,
					Retrieve: AlwaysIncluded,
				}}
				break
			}
			cTerm.Retrieve = AlwaysIncluded
		}
		if cTerm.Match == nil {
			cTerm.Match = matchAlways
		}
		result.Terms = append(result.Terms, cTerm)
	}
	return result
}

func metaList2idSet(ml []*meta.Meta) id.Set {
	if ml == nil {
		return nil
	}
	result := id.NewSetCap(len(ml))
	for _, m := range ml {
		result = result.Add(m.Zid)
	}
	return result
}

func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet id.Set) CompiledTerm {
	match := ct.compileMeta() // Match might add some searches
	var pred RetrievePredicate
	if searcher != nil {
		pred = ct.retrieveIndex(searcher)
		if startSet != nil {
			if pred == nil {
				pred = startSet.ContainsOrNil
			} else {
				predSet := id.NewSetCap(len(startSet))
				for zid := range startSet {
					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.ContainsOrNil(zid) }
	}
	if len(positives) == 0 {
		// 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.ContainsOrNil
	}
	negatives := retrieveNegatives(negCalls)
	if negatives == nil {
		return positives.ContainsOrNil
	}
	return func(zid id.Zid) bool {
		return positives.ContainsOrNil(zid) && !negatives.ContainsOrNil(zid)
	}
}

// Limit returns only s.GetLimit() elements of the given list.
func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta {
	if q == nil {
		return metaList
	}
	return limitElements(metaList, q.limit)
}

Added 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 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/strfun"
	"zettelstore.de/z/zettel/id"
)

type searchOp struct {
	s  string
	op compareOp
}
type searchFunc func(string) id.Set
type searchCallMap map[searchOp]searchFunc

var cmpPred = map[compareOp]func(string, string) bool{
	cmpEqual:   stringEqual,
	cmpPrefix:  strings.HasPrefix,
	cmpSuffix:  strings.HasSuffix,
	cmpMatch:   strings.Contains,
	cmpHas:     strings.Contains, // the "has" operator have string semantics here in a index search
	cmpLess:    strings.Contains, // in index search there is no "less", only "has"
	cmpGreater: strings.Contains, // in index search there is no "greater", only "has"
}

func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) {
	pred := cmpPred[op]
	for k := range scm {
		if op == cmpMatch {
			if strings.Contains(k.s, s) {
				return
			}
			if strings.Contains(s, k.s) {
				delete(scm, k)
				break
			}
		}
		if k.op != op {
			continue
		}
		if pred(k.s, s) {
			return
		}
		if pred(s, k.s) {
			delete(scm, k)
		}
	}
	scm[searchOp{s: s, op: op}] = sf
}

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()
				negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
			} else {
				normCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
			}
		}
	}

	plainCalls = make(searchCallMap, len(search))
	for _, val := range search {
		word := strings.ToLower(strings.TrimSpace(val.value))
		if cmpOp := val.op; cmpOp.isNegated() {
			cmpOp = cmpOp.negate()
			negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
		} else {
			plainCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
		}
	}
	return normCalls, plainCalls, negCalls
}

func hasConflictingCalls(normCalls, plainCalls, negCalls searchCallMap) bool {
	for val := range negCalls {
		if _, found := normCalls[val]; found {
			return true
		}
		if _, found := plainCalls[val]; found {
			return true
		}
	}
	return false
}

func retrievePositives(normCalls, plainCalls searchCallMap) id.Set {
	if isSuperset(normCalls, plainCalls) {
		var normResult id.Set
		for c, sf := range normCalls {
			normResult = normResult.IntersectOrSet(sf(c.s))
		}
		return normResult
	}

	type searchResults map[searchOp]id.Set
	var cache searchResults
	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
	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.Copy(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
	for val, sf := range negCalls {
		negatives = negatives.Copy(sf(val.s))
	}
	return negatives
}

func getSearchFunc(searcher Searcher, op compareOp) searchFunc {
	switch op {
	case cmpEqual:
		return searcher.SearchEqual
	case cmpPrefix:
		return searcher.SearchPrefix
	case cmpSuffix:
		return searcher.SearchSuffix
	case cmpMatch, cmpHas, cmpLess, cmpGreater: // for index search we assume string semantics
		return searcher.SearchContains
	default:
		panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op))
	}
}

Added 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
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 (
	"fmt"
	"strconv"
	"strings"
	"unicode/utf8"

	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

type matchValueFunc func(value string) bool

func matchValueNever(string) bool { return false }

type matchSpec struct {
	key   string
	match matchValueFunc
}

// compileMeta calculates a selection func based on the given select criteria.
func (ct *conjTerms) compileMeta() MetaMatchFunc {
	for key, vals := range ct.mvals {
		// All queried keys must exist, if there is at least one non-negated compare operation
		//
		// This is only an optimization to make selection of metadata faster.
		if countNegatedOps(vals) < len(vals) {
			ct.addKey(key, cmpExist)
		}
	}
	for _, op := range ct.keys {
		if op != cmpExist && op != cmpNotExist {
			return matchNever
		}
	}
	posSpecs, negSpecs := ct.createSelectSpecs()
	if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 {
		return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys)
	}
	return nil
}

func countNegatedOps(vals []expValue) int {
	count := 0
	for _, val := range vals {
		if val.op.isNegated() {
			count++
		}
	}
	return count
}

func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) {
	posSpecs = make([]matchSpec, 0, len(ct.mvals))
	negSpecs = make([]matchSpec, 0, len(ct.mvals))
	for key, values := range ct.mvals {
		if !meta.KeyIsValid(key) {
			continue
		}
		posMatch, negMatch := createPosNegMatchFunc(key, values, ct.addSearch)
		if posMatch != nil {
			posSpecs = append(posSpecs, matchSpec{key, posMatch})
		}
		if negMatch != nil {
			negSpecs = append(negSpecs, matchSpec{key, negMatch})
		}
	}
	return posSpecs, negSpecs
}

type addSearchFunc func(val expValue)

func noAddSearch(expValue) { /* Just does nothing, for negated queries */ }

func createPosNegMatchFunc(key string, values []expValue, addSearch addSearchFunc) (posMatch, negMatch matchValueFunc) {
	posValues := make([]expValue, 0, len(values))
	negValues := make([]expValue, 0, len(values))
	for _, val := range values {
		if val.op.isNegated() {
			negValues = append(negValues, val)
		} else {
			posValues = append(posValues, val)
		}
	}
	if meta.IsProperty(key) {
		// Properties are not stored in the Zettelstore and in the search index.
		addSearch = noAddSearch
	}
	return createMatchFunc(key, posValues, addSearch), createMatchFunc(key, negValues, addSearch)
}

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:
		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 createMatchWordFunc(values, addSearch)
	case meta.TypeZettelmarkup:
		return createMatchZmkFunc(values, addSearch)
	}
	return createMatchStringFunc(values, addSearch)
}

func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToIDPredicates(values, addSearch)
	return func(value string) bool {
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := 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
			}
		}
		return true
	}
}

func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToNumberPredicates(values, addSearch)
	return func(value string) bool {
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := 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
				}
			}
		}
		return true
	}
}

func processTagSet(valueSet [][]expValue) [][]expValue {
	result := make([][]expValue, len(valueSet))
	for i, values := range valueSet {
		tags := make([]expValue, len(values))
		for j, val := range values {
			if tval := val.value; tval != "" && tval[0] == '#' {
				tval = meta.CleanTag(tval)
				tags[j] = expValue{value: tval, op: val.op}
			} else {
				tags[j] = expValue{value: tval, op: val.op}
			}
		}
		result[i] = tags
	}
	return result
}

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 createMatchStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToStringPredicates(sliceToLower(values), addSearch)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func sliceToLower(sl []expValue) []expValue {
	result := make([]expValue, 0, len(sl))
	for _, s := range sl {
		result = append(result, expValue{
			value: strings.ToLower(s.value),
			op:    s.op,
		})
	}
	return result
}

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, createWordCompareFunc(word, cmpOp))
			} else {
				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))
		for _, s := range temp {
			values = append(values, strfun.NormalizeWords(s)...)
		}
		for _, pred := range normPreds {
			if noneOf(pred, values) {
				return false
			}
		}
		for _, pred := range negPreds {
			for _, val := range values {
				if pred(val) {
					return false
				}
			}
		}
		return true
	}
}

func noneOf(pred stringPredicate, values []string) bool {
	for _, value := range values {
		if pred(value) {
			return false
		}
	}
	return true
}

func zmk2text(zmk string) string {
	isASCII, hasUpper, needParse := true, false, false
	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 sb strings.Builder
	if _, err := textenc.Create().WriteInlines(&sb, &is); err != nil {
		return strings.ToLower(zmk)
	}
	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))
		for _, se := range splitElems {
			e := strings.TrimSpace(se)
			if len(e) > 0 {
				valueElems = append(valueElems, expValue{value: e, op: elem.op})
			}
		}
		if len(valueElems) > 0 {
			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 !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] = 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 createWordCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	switch cmpOp {
	case cmpEqual:
		return func(metaVal string) bool { return metaVal == cmpVal }
	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 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 op := disambiguateWordOp(v.op); op {
			case cmpEqual:
				addSearch(v) // addSearch only for positive selections
				fallthrough
			case cmpNotEqual:
				elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, op == cmpEqual)
			case cmpPrefix:
				addSearch(v)
				fallthrough
			case cmpNoPrefix:
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, op == cmpPrefix)
			case cmpSuffix:
				addSearch(v)
				fallthrough
			case cmpNoSuffix:
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, op == cmpSuffix)
			case cmpMatch:
				addSearch(v)
				fallthrough
			case cmpNoMatch:
				elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, op == cmpMatch)
			case cmpLess, cmpNoLess:
				elemPreds[j] = makeStringSetPredicate(opVal, stringLess, op == cmpLess)
			case cmpGreater, cmpNoGreater:
				elemPreds[j] = makeStringSetPredicate(opVal, stringGreater, op == cmpGreater)
			case cmpHas, cmpHasNot:
				panic(fmt.Sprintf("operator %d not disambiguated with value %q", op, opVal))
			default:
				panic(fmt.Sprintf("Unknown compare operation %d with value %q", op, opVal))
			}
		}
		result[i] = elemPreds
	}
	return result
}

func stringEqual(val1, val2 string) bool   { return val1 == val2 }
func stringLess(val1, val2 string) bool    { return val1 < val2 }
func stringGreater(val1, val2 string) bool { return val1 > val2 }

type compareStringFunc func(val1, val2 string) bool

func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate {
	return func(metaVals []string) bool {
		for _, metaVal := range metaVals {
			if compare(metaVal, neededValue) {
				return foundResult
			}
		}
		return !foundResult
	}
}

func makeSearchMetaMatchFunc(posSpecs, negSpecs []matchSpec, kem keyExistMap) MetaMatchFunc {
	// Optimize: no specs --> just check kwhether key exists
	if len(posSpecs) == 0 && len(negSpecs) == 0 {
		if len(kem) == 0 {
			return nil
		}
		return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) }
	}

	// Optimize: only negative or only positive matching
	if len(posSpecs) == 0 {
		return func(m *meta.Meta) bool {
			return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, negSpecs)
		}
	}
	if len(negSpecs) == 0 {
		return func(m *meta.Meta) bool {
			return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, posSpecs)
		}
	}

	return func(m *meta.Meta) bool {
		return matchMetaKeyExists(m, kem) &&
			matchMetaSpecs(m, posSpecs) &&
			matchMetaSpecs(m, negSpecs)
	}
}

func matchMetaKeyExists(m *meta.Meta, kem keyExistMap) bool {
	for key, op := range kem {
		_, found := m.Get(key)
		if found != (op == cmpExist) {
			return false
		}
	}
	return true
}
func matchMetaSpecs(m *meta.Meta, specs []matchSpec) bool {
	for _, s := range specs {
		if value := m.GetDefault(s.key, ""); !s.match(value) {
			return false
		}
	}
	return true
}

Added 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package query_test

import (
	"context"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func TestMatchZidNegate(t *testing.T) {
	q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense))
	compiled := q.RetrieveAndCompile(context.Background(), nil, nil)

	testCases := []struct {
		zid api.ZettelID
		exp bool
	}{
		{api.ZidVersion, false},
		{api.ZidLicense, false},
		{api.ZidAuthors, true},
	}
	for i, tc := range testCases {
		m := meta.New(id.MustParse(tc.zid))
		if compiled.Terms[0].Match(m) != tc.exp {
			if tc.exp {
				t.Errorf("%d: meta %v must match %q", i, m.Zid, q)
			} else {
				t.Errorf("%d: meta %v must not match %q", i, m.Zid, q)
			}
		}
	}
}

Added 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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 (
	"strconv"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/meta"
)

type sortFunc func(i, j int) bool

func createSortFunc(order []sortOrder, ml []*meta.Meta) sortFunc {
	hasID := false
	sortFuncs := make([]sortFunc, 0, len(order)+1)
	for _, o := range order {
		sortFuncs = append(sortFuncs, createOneSortFunc(o.key, o.descending, ml))
		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 })
	}
	// return sortFuncs[0]
	if len(sortFuncs) == 1 {
		return sortFuncs[0]
	}
	return func(i, j int) bool {
		for _, sf := range sortFuncs {
			if sf(i, j) {
				return true
			}
			if sf(j, i) {
				return false
			}
		}
		return false
	}
}

func createOneSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc {
	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 }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
	}
	if keyType == meta.TypeTimestamp {
		return createSortTimestampFunc(ml, key, descending)
	}
	if keyType == meta.TypeNumber {
		return createSortNumberFunc(ml, key, descending)
	}
	return createSortStringFunc(ml, key, descending)
}

func createSortTimestampFunc(ml []*meta.Meta, 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 || meta.ExpandTimestamp(iVal) > meta.ExpandTimestamp(jVal))) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := ml[i].Get(key)
		jVal, jOk := ml[j].Get(key)
		return (iOk && (!jOk || meta.ExpandTimestamp(iVal) < meta.ExpandTimestamp(jVal))) || !jOk
	}
}

func createSortNumberFunc(ml []*meta.Meta, 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 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(ml []*meta.Meta, 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
	}
}

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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 "zettelstore.de/client.fossil/api"

// IdentSpec contains all specification values to calculate the ident directive.
type IdentSpec struct{}

func (spec *IdentSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.IdentDirective)
}

// ItemsSpec contains all specification values to calculate items.
type ItemsSpec struct{}

func (spec *ItemsSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ItemsDirective)
}

Added query/unlinked.go.



















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
//-----------------------------------------------------------------------------

package query

import (
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

// UnlinkedSpec contains all specification values to calculate unlinked references.
type UnlinkedSpec struct {
	words []string
}

func (spec *UnlinkedSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.UnlinkedDirective)
	for _, word := range spec.words {
		pe.writeStrings(" ", api.PhraseDirective, " ", word)
	}
}

func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string {
	if words := spec.words; len(words) > 0 {
		result := make([]string, len(words))
		copy(result, words)
		return result
	}
	result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title
	for _, m := range metaSeq {
		title, hasTitle := m.Get(api.KeyTitle)
		if !hasTitle {
			continue
		}
		result = append(result, strfun.MakeWords(title)...)
	}
	return result
}

Deleted runes/runes.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21





















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package runes provides some functions on runes.
package runes

// IsSpace returns true if rune is a whitespace.
func IsSpace(ch rune) bool {
	switch ch {
	case ' ', '\t':
		return true
	}
	return false
}

Deleted search/filter.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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



















































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package search provides a zettel search.
package search

import (
	"strings"

	"zettelstore.de/z/domain/meta"
)

type matchFunc func(value string) bool

func matchNever(value string) bool  { return false }
func matchAlways(value string) bool { return true }

type matchSpec struct {
	key   string
	match matchFunc
}

// compileFilter calculates a filter func based on the given filter.
func compileFilter(tags expTagValues) MetaMatchFunc {
	specs, nomatch := createFilterSpecs(tags)
	if len(specs) == 0 && len(nomatch) == 0 {
		return nil
	}
	return makeSearchMetaFilterFunc(specs, nomatch)
}

func createFilterSpecs(tags map[string][]expValue) ([]matchSpec, []string) {
	specs := make([]matchSpec, 0, len(tags))
	var nomatch []string
	for key, values := range tags {
		if !meta.KeyIsValid(key) {
			continue
		}
		if empty, negates := hasEmptyValues(values); empty {
			if negates == 0 {
				specs = append(specs, matchSpec{key, matchAlways})
				continue
			}
			if len(values) < negates {
				specs = append(specs, matchSpec{key, matchNever})
				continue
			}
			nomatch = append(nomatch, key)
			continue
		}
		match := createMatchFunc(key, values)
		if match != nil {
			specs = append(specs, matchSpec{key, match})
		}
	}
	return specs, nomatch
}

func hasEmptyValues(values []expValue) (bool, int) {
	var negates int
	for _, v := range values {
		if v.value != "" {
			continue
		}
		if !v.negate {
			return true, 0
		}
		negates++
	}
	return negates > 0, negates
}

func createMatchFunc(key string, values []expValue) matchFunc {
	switch meta.Type(key) {
	case meta.TypeBool:
		return createMatchBoolFunc(values)
	case meta.TypeCredential:
		return matchNever
	case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout
		return createMatchIDFunc(values)
	case meta.TypeIDSet:
		return createMatchIDSetFunc(values)
	case meta.TypeTagSet:
		return createMatchTagSetFunc(values)
	case meta.TypeWord:
		return createMatchWordFunc(values)
	case meta.TypeWordSet:
		return createMatchWordSetFunc(values)
	}
	return createMatchStringFunc(values)
}

func createMatchBoolFunc(values []expValue) matchFunc {
	preValues := make([]bool, 0, len(values))
	for _, v := range values {
		boolValue := meta.BoolValue(v.value)
		if v.negate {
			boolValue = !boolValue
		}
		preValues = append(preValues, boolValue)
	}
	return func(value string) bool {
		bValue := meta.BoolValue(value)
		for _, v := range preValues {
			if bValue != v {
				return false
			}
		}
		return true
	}
}

func createMatchIDFunc(values []expValue) matchFunc {
	return func(value string) bool {
		for _, v := range values {
			if strings.HasPrefix(value, v.value) == v.negate {
				return false
			}
		}
		return true
	}
}

func createMatchIDSetFunc(values []expValue) matchFunc {
	idValues := preprocessSet(sliceToLower(values))
	return func(value string) bool {
		ids := meta.ListFromValue(value)
		for _, neededIDs := range idValues {
			for _, neededID := range neededIDs {
				if matchAllID(ids, neededID.value) == neededID.negate {
					return false
				}
			}
		}
		return true
	}
}

func matchAllID(zettelIDs []string, neededID string) bool {
	for _, zt := range zettelIDs {
		if strings.HasPrefix(zt, neededID) {
			return true
		}
	}
	return false
}

func createMatchTagSetFunc(values []expValue) matchFunc {
	tagValues := processTagSet(preprocessSet(values))
	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 _, neededTags := range tagValues {
			for _, neededTag := range neededTags {
				if matchAllTag(tags, neededTag.value, neededTag.equal) == neededTag.negate {
					return false
				}
			}
		}
		return true
	}
}

type tagQueryValue struct {
	value  string
	negate bool
	equal  bool // not equal == prefix
}

func processTagSet(valueSet [][]expValue) [][]tagQueryValue {
	result := make([][]tagQueryValue, len(valueSet))
	for i, values := range valueSet {
		tags := make([]tagQueryValue, len(values))
		for j, val := range values {
			if tval := val.value; tval != "" && tval[0] == '#' {
				tval = meta.CleanTag(tval)
				tags[j] = tagQueryValue{value: tval, negate: val.negate, equal: true}
			} else {
				tags[j] = tagQueryValue{value: tval, negate: val.negate, equal: false}
			}
		}
		result[i] = tags
	}
	return result
}

func matchAllTag(zettelTags []string, neededTag string, equal bool) bool {
	if equal {
		for _, zt := range zettelTags {
			if zt == neededTag {
				return true
			}
		}
	} else {
		for _, zt := range zettelTags {
			if strings.HasPrefix(zt, neededTag) {
				return true
			}
		}
	}
	return false
}

func createMatchWordFunc(values []expValue) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if (value == v.value) == v.negate {
				return false
			}
		}
		return true
	}
}

func createMatchWordSetFunc(values []expValue) matchFunc {
	wordValues := preprocessSet(sliceToLower(values))
	return func(value string) bool {
		words := meta.ListFromValue(value)
		for _, neededWords := range wordValues {
			for _, neededWord := range neededWords {
				if matchAllWord(words, neededWord.value) == neededWord.negate {
					return false
				}
			}
		}
		return true
	}
}

func createMatchStringFunc(values []expValue) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if strings.Contains(value, v.value) == v.negate {
				return false
			}
		}
		return true
	}
}

func makeSearchMetaFilterFunc(specs []matchSpec, nomatch []string) MetaMatchFunc {
	return func(m *meta.Meta) bool {
		for _, s := range specs {
			if value, ok := m.Get(s.key); !ok || !s.match(value) {
				return false
			}
		}
		for _, key := range nomatch {
			if _, ok := m.Get(key); ok {
				return false
			}
		}
		return true
	}
}

func sliceToLower(sl []expValue) []expValue {
	result := make([]expValue, 0, len(sl))
	for _, s := range sl {
		result = append(result, expValue{
			value:  strings.ToLower(s.value),
			negate: s.negate,
		})
	}
	return result
}

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))
		for _, se := range splitElems {
			e := strings.TrimSpace(se)
			if len(e) > 0 {
				valueElems = append(valueElems, expValue{value: e, negate: elem.negate})
			}
		}
		if len(valueElems) > 0 {
			result = append(result, valueElems)
		}
	}
	return result
}

func matchAllWord(zettelWords []string, neededWord string) bool {
	for _, zw := range zettelWords {
		if zw == neededWord {
			return true
		}
	}
	return false
}

Deleted search/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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119























































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package search provides a zettel search.
package search

import (
	"io"
	"sort"
	"strconv"

	"zettelstore.de/z/domain/meta"
)

// Print the filter to a writer.
func (s *Search) Print(w io.Writer) {
	if s.negate {
		io.WriteString(w, "NOT (")
	}
	space := false
	if len(s.search) > 0 {
		io.WriteString(w, "ANY")
		printFilterExprValues(w, s.search)
		space = true
	}
	names := make([]string, 0, len(s.tags))
	for name := range s.tags {
		names = append(names, name)
	}
	sort.Strings(names)
	for _, name := range names {
		if space {
			io.WriteString(w, " AND ")
		}
		io.WriteString(w, name)
		printFilterExprValues(w, s.tags[name])
		space = true
	}
	if s.negate {
		io.WriteString(w, ")")
		space = true
	}

	if ord := s.order; len(ord) > 0 {
		switch ord {
		case meta.KeyID:
			// Ignore
		case RandomOrder:
			space = printSpace(w, space)
			io.WriteString(w, "RANDOM")
		default:
			space = printSpace(w, space)
			io.WriteString(w, "SORT ")
			io.WriteString(w, ord)
			if s.descending {
				io.WriteString(w, " DESC")
			}
		}
	}
	if off := s.offset; off > 0 {
		space = printSpace(w, space)
		io.WriteString(w, "OFFSET ")
		io.WriteString(w, strconv.Itoa(off))
	}
	if lim := s.limit; lim > 0 {
		_ = printSpace(w, space)
		io.WriteString(w, "LIMIT ")
		io.WriteString(w, strconv.Itoa(lim))
	}
}

func printFilterExprValues(w io.Writer, values []expValue) {
	if len(values) == 0 {
		io.WriteString(w, " MATCH ANY")
		return
	}

	for j, val := range values {
		if j > 0 {
			io.WriteString(w, " AND")
		}
		if val.negate {
			io.WriteString(w, " NOT")
		}
		switch val.op {
		case cmpDefault:
			io.WriteString(w, " MATCH ")
		case cmpEqual:
			io.WriteString(w, " HAS ")
		case cmpPrefix:
			io.WriteString(w, " PREFIX ")
		case cmpSuffix:
			io.WriteString(w, " SUFFIX ")
		case cmpContains:
			io.WriteString(w, " CONTAINS ")
		default:
			io.WriteString(w, " MaTcH ")
		}
		if val.value == "" {
			io.WriteString(w, "ANY")
		} else {
			io.WriteString(w, val.value)
		}
	}
}

func printSpace(w io.Writer, space bool) bool {
	if space {
		io.WriteString(w, " ")
	}
	return true
}

Deleted search/search.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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




















































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package search provides a zettel search.
package search

import (
	"math/rand"
	"sort"
	"strings"
	"sync"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// Selector is used to select zettel identifier based on selection criteria.
type Selector interface {
	// Select all zettel that contains the given exact word.
	// The word must be normalized through Unicode NKFD, trimmed and not empty.
	SelectEqual(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.
	SelectPrefix(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.
	SelectSuffix(suffix string) id.Set

	// Select all zettel that contains the given string.
	// The string must be normalized through Unicode NKFD, trimmed and not empty.
	SelectContains(s string) id.Set
}

// MetaMatchFunc is a function determine whethe some metadata should be filtered or not.
type MetaMatchFunc func(*meta.Meta) bool

// Search specifies a mechanism for selecting zettel.
type Search struct {
	mx sync.RWMutex // Protects other attributes

	// Fields to be used for filtering
	preMatch MetaMatchFunc // Match that must be true
	tags     expTagValues  // Expected values for a tag
	search   []expValue    // Search string
	negate   bool          // Negate the result of the whole filtering process

	// Fields to be used for sorting
	order      string // Name of meta key. None given: use "id"
	descending bool   // Sort by order, but descending
	offset     int    // <= 0: no offset
	limit      int    // <= 0: no limit
}

type expTagValues map[string][]expValue

// RandomOrder is a pseudo metadata key that selects a random order.
const RandomOrder = "_random"

type compareOp uint8

const (
	cmpDefault compareOp = iota
	cmpEqual
	cmpPrefix
	cmpSuffix
	cmpContains
)

type expValue struct {
	value  string
	op     compareOp
	negate bool
}

// AddExpr adds a match expression to the filter.
func (s *Search) AddExpr(key, val string) *Search {
	val, negate, op := parseOp(strings.TrimSpace(val))
	if s == nil {
		s = new(Search)
	}
	s.mx.Lock()
	defer s.mx.Unlock()
	if key == "" {
		s.search = append(s.search, expValue{value: val, op: op, negate: negate})
	} else if s.tags == nil {
		s.tags = expTagValues{key: {{value: val, op: op, negate: negate}}}
	} else {
		s.tags[key] = append(s.tags[key], expValue{value: val, op: op, negate: negate})
	}
	return s
}

func parseOp(s string) (r string, negate bool, op compareOp) {
	if s == "" {
		return s, false, cmpDefault
	}
	if s[0] == '\\' {
		return s[1:], false, cmpDefault
	}
	if s[0] == '!' {
		negate = true
		s = s[1:]
	}
	if s == "" {
		return s, negate, cmpDefault
	}
	if s[0] == '\\' {
		return s[1:], negate, cmpDefault
	}
	switch s[0] {
	case ':':
		return s[1:], negate, cmpDefault
	case '=':
		return s[1:], negate, cmpEqual
	case '>':
		return s[1:], negate, cmpPrefix
	case '<':
		return s[1:], negate, cmpSuffix
	case '~':
		return s[1:], negate, cmpContains
	}
	return s, negate, cmpDefault
}

// SetNegate changes the filter to reverse its selection.
func (s *Search) SetNegate() *Search {
	if s == nil {
		s = new(Search)
	}
	s.mx.Lock()
	defer s.mx.Unlock()
	s.negate = true
	return s
}

// AddPreMatch adds the pre-filter selection predicate.
func (s *Search) AddPreMatch(preMatch MetaMatchFunc) *Search {
	if s == nil {
		s = new(Search)
	}
	s.mx.Lock()
	defer s.mx.Unlock()
	if pre := s.preMatch; pre == nil {
		s.preMatch = preMatch
	} else {
		s.preMatch = func(m *meta.Meta) bool {
			return preMatch(m) && pre(m)
		}
	}
	return s
}

// AddOrder adds the given order to the search object.
func (s *Search) AddOrder(key string, descending bool) *Search {
	if s == nil {
		s = new(Search)
	}
	s.mx.Lock()
	defer s.mx.Unlock()
	if s.order != "" {
		panic("order field already set: " + s.order)
	}
	s.order = key
	s.descending = descending
	return s
}

// SetOffset sets the given offset of the search object.
func (s *Search) SetOffset(offset int) *Search {
	if s == nil {
		s = new(Search)
	}
	s.mx.Lock()
	defer s.mx.Unlock()
	if offset < 0 {
		offset = 0
	}
	s.offset = offset
	return s
}

// GetOffset returns the current offset value.
func (s *Search) GetOffset() int {
	if s == nil {
		return 0
	}
	s.mx.RLock()
	defer s.mx.RUnlock()
	return s.offset
}

// SetLimit sets the given limit of the search object.
func (s *Search) SetLimit(limit int) *Search {
	if s == nil {
		s = new(Search)
	}
	s.mx.Lock()
	defer s.mx.Unlock()
	if limit < 0 {
		limit = 0
	}
	s.limit = limit
	return s
}

// GetLimit returns the current offset value.
func (s *Search) GetLimit() int {
	if s == nil {
		return 0
	}
	s.mx.RLock()
	defer s.mx.RUnlock()
	return s.limit
}

// HasComputedMetaKey returns true, if the filter references a metadata key which
// a computed value.
func (s *Search) HasComputedMetaKey() bool {
	if s == nil {
		return false
	}
	s.mx.RLock()
	defer s.mx.RUnlock()
	for key := range s.tags {
		if meta.IsComputed(key) {
			return true
		}
	}
	if order := s.order; order != "" && meta.IsComputed(order) {
		return true
	}
	return false
}

// CompileMatch returns a function to match meta data based on filter specification.
func (s *Search) CompileMatch(selector Selector) MetaMatchFunc {
	if s == nil {
		return filterNone
	}
	s.mx.Lock()
	defer s.mx.Unlock()

	compMeta := compileFilter(s.tags)
	compSearch := compileFullSearch(selector, s.search)
	if preMatch := s.preMatch; preMatch != nil {
		return compilePreMatch(preMatch, compMeta, compSearch, s.negate)
	}
	return compileNoPreMatch(compMeta, compSearch, s.negate)
}

func filterNone(m *meta.Meta) bool { return true }

func compilePreMatch(preMatch, compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc {
	if compMeta == nil {
		if compSearch == nil {
			return preMatch
		}
		if negate {
			return func(m *meta.Meta) bool { return preMatch(m) && !compSearch(m) }
		}
		return func(m *meta.Meta) bool { return preMatch(m) && compSearch(m) }
	}
	if compSearch == nil {
		if negate {
			return func(m *meta.Meta) bool { return preMatch(m) && !compMeta(m) }
		}
		return func(m *meta.Meta) bool { return preMatch(m) && compMeta(m) }
	}
	if negate {
		return func(m *meta.Meta) bool { return preMatch(m) && (!compMeta(m) || !compSearch(m)) }
	}
	return func(m *meta.Meta) bool { return preMatch(m) && compMeta(m) && compSearch(m) }
}

func compileNoPreMatch(compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc {
	if compMeta == nil {
		if compSearch == nil {
			if negate {
				return func(m *meta.Meta) bool { return false }
			}
			return filterNone
		}
		if negate {
			return func(m *meta.Meta) bool { return !compSearch(m) }
		}
		return compSearch
	}
	if compSearch == nil {
		if negate {
			return func(m *meta.Meta) bool { return !compMeta(m) }
		}
		return compMeta
	}
	if negate {
		return func(m *meta.Meta) bool { return !compMeta(m) || !compSearch(m) }
	}
	return func(m *meta.Meta) bool { return compMeta(m) && compSearch(m) }
}

// Sort applies the sorter to the slice of meta data.
func (s *Search) Sort(metaList []*meta.Meta) []*meta.Meta {
	if len(metaList) == 0 {
		return metaList
	}

	if s == nil {
		sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
		return metaList
	}

	if s.order == "" {
		sort.Slice(metaList, createSortFunc(meta.KeyID, true, metaList))
	} else if s.order == RandomOrder {
		rand.Shuffle(len(metaList), func(i, j int) {
			metaList[i], metaList[j] = metaList[j], metaList[i]
		})
	} else {
		sort.Slice(metaList, createSortFunc(s.order, s.descending, metaList))
	}

	if s.offset > 0 {
		if s.offset > len(metaList) {
			return nil
		}
		metaList = metaList[s.offset:]
	}
	if s.limit > 0 && s.limit < len(metaList) {
		metaList = metaList[:s.limit]
	}
	return metaList
}

Deleted search/selector.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179



















































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package search provides a zettel search.
package search

// This file is about "compiling" a search expression into a function.

import (
	"fmt"
	"strings"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/strfun"
)

func compileFullSearch(selector Selector, search []expValue) MetaMatchFunc {
	normSearch := compileNormalizedSearch(selector, search)
	plainSearch := compilePlainSearch(selector, search)
	if normSearch == nil {
		if plainSearch == nil {
			return nil
		}
		return plainSearch
	}
	if plainSearch == nil {
		return normSearch
	}
	return func(m *meta.Meta) bool {
		return normSearch(m) || plainSearch(m)
	}
}

func compileNormalizedSearch(selector Selector, search []expValue) MetaMatchFunc {
	var positives, negatives []expValue
	posSet := make(map[string]bool)
	negSet := make(map[string]bool)
	for _, val := range search {
		for _, word := range strfun.NormalizeWords(val.value) {
			if val.negate {
				if _, ok := negSet[word]; !ok {
					negSet[word] = true
					negatives = append(negatives, expValue{
						value:  word,
						op:     val.op,
						negate: true,
					})
				}
			} else {
				if _, ok := posSet[word]; !ok {
					posSet[word] = true
					positives = append(positives, expValue{
						value:  word,
						op:     val.op,
						negate: false,
					})
				}
			}
		}
	}
	return compileSearch(selector, positives, negatives)
}
func compilePlainSearch(selector Selector, search []expValue) MetaMatchFunc {
	var positives, negatives []expValue
	for _, val := range search {
		if val.negate {
			negatives = append(negatives, expValue{
				value:  strings.ToLower(strings.TrimSpace(val.value)),
				op:     val.op,
				negate: true,
			})
		} else {
			positives = append(positives, expValue{
				value:  strings.ToLower(strings.TrimSpace(val.value)),
				op:     val.op,
				negate: false,
			})
		}
	}
	return compileSearch(selector, positives, negatives)
}

func compileSearch(selector Selector, poss, negs []expValue) MetaMatchFunc {
	if len(poss) == 0 {
		if len(negs) == 0 {
			return nil
		}
		return makeNegOnlySearch(selector, negs)
	}
	if len(negs) == 0 {
		return makePosOnlySearch(selector, poss)
	}
	return makePosNegSearch(selector, poss, negs)
}

func makePosOnlySearch(selector Selector, poss []expValue) MetaMatchFunc {
	retrievePos := compileRetrieveZids(selector, poss)
	var ids id.Set
	return func(m *meta.Meta) bool {
		if ids == nil {
			ids = retrievePos()
		}
		_, ok := ids[m.Zid]
		return ok
	}
}

func makeNegOnlySearch(selector Selector, negs []expValue) MetaMatchFunc {
	retrieveNeg := compileRetrieveZids(selector, negs)
	var ids id.Set
	return func(m *meta.Meta) bool {
		if ids == nil {
			ids = retrieveNeg()
		}
		_, ok := ids[m.Zid]
		return !ok
	}
}

func makePosNegSearch(selector Selector, poss, negs []expValue) MetaMatchFunc {
	retrievePos := compileRetrieveZids(selector, poss)
	retrieveNeg := compileRetrieveZids(selector, negs)
	var ids id.Set
	return func(m *meta.Meta) bool {
		if ids == nil {
			ids = retrievePos()
			ids.Remove(retrieveNeg())
		}
		_, okPos := ids[m.Zid]
		return okPos
	}
}

func compileRetrieveZids(selector Selector, values []expValue) func() id.Set {
	selFuncs := make([]selectorFunc, 0, len(values))
	stringVals := make([]string, 0, len(values))
	for _, val := range values {
		selFuncs = append(selFuncs, compileSelectOp(selector, val.op))
		stringVals = append(stringVals, val.value)
	}
	if len(selFuncs) == 0 {
		return func() id.Set { return id.NewSet() }
	}
	if len(selFuncs) == 1 {
		return func() id.Set { return selFuncs[0](stringVals[0]) }
	}
	return func() id.Set {
		result := selFuncs[0](stringVals[0])
		for i, f := range selFuncs[1:] {
			result = result.Intersect(f(stringVals[i+1]))
		}
		return result
	}
}

type selectorFunc func(string) id.Set

func compileSelectOp(selector Selector, op compareOp) selectorFunc {
	switch op {
	case cmpDefault, cmpContains:
		return selector.SelectContains
	case cmpEqual:
		return selector.SelectEqual
	case cmpPrefix:
		return selector.SelectPrefix
	case cmpSuffix:
		return selector.SelectSuffix
	default:
		panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op))
	}
}

Deleted search/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































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package search provides a zettel search.
package search

import (
	"strconv"

	"zettelstore.de/z/domain/meta"
)

type sortFunc func(i, j int) bool

func createSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc {
	keyType := meta.Type(key)
	if key == meta.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
	}
	if keyType == meta.TypeBool {
		return createSortBoolFunc(ml, key, descending)
	}
	if keyType == meta.TypeNumber {
		return createSortNumberFunc(ml, key, descending)
	}
	return createSortStringFunc(ml, key, descending)
}

func createSortBoolFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			left := ml[i].GetBool(key)
			if left == ml[j].GetBool(key) {
				return i > j
			}
			return left
		}
	}
	return func(i, j int) bool {
		right := ml[j].GetBool(key)
		if ml[i].GetBool(key) == right {
			return i < j
		}
		return right
	}
}

func createSortNumberFunc(ml []*meta.Meta, 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 {
	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
	}
}

func getNum(m *meta.Meta, key string) (int, bool) {
	if s, ok := m.Get(key); ok {
		if i, err := strconv.Atoi(s); err == nil {
			return i, true
		}
	}
	return 0, false
}

Changes to strfun/escape.go.

1
2

3
4

5
6
7
8



9
10
11
12
13
14
15
16
17
18
19
20
21
22







23
24
25

26
27



28
29
30

31
32

33
34
35


36
37
38

39
40
41
42


43
44

45
46

47
48
49
50
51
52
53
54
55
56
57
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

-
+

-
+




+
+
+


-





-
-
-
-
-
-
+
+
+
+
+
+
+


-
+
-
-
+
+
+

-
-
+
-
-
+

-
-
+
+
-
-
-
+
-
-
-
-
+
+

-
+

-
+

-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-




-
+




//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package strfun provides some string functions.
package strfun

import "io"

var (
	htmlQuot     = []byte("&quot;") // shorter than "&39;", but often requested in standards
	htmlAmp      = []byte("&amp;")
	htmlLt       = []byte("&lt;")
	htmlGt       = []byte("&gt;")
	htmlNull     = []byte("\uFFFD")
	htmlVisSpace = []byte("\u2423")
	escQuot = []byte("&quot;") // longer than "&#34;", but often requested in standards
	escAmp  = []byte("&amp;")
	escApos = []byte("apos;") // longer than "&#39", but sometimes requested in tests
	escLt   = []byte("&lt;")
	escGt   = []byte("&gt;")
	escTab  = []byte("&#9;")
	escNull = []byte("\uFFFD")
)

// HTMLEscape writes to w the escaped HTML equivalent of the given string.
// XMLEscape writes the string to the given writer, where every rune that has a special
// If visibleSpace is true, each space is written as U-2423.
func HTMLEscape(w io.Writer, s string, visibleSpace bool) {
// meaning in XML is escaped.
func XMLEscape(w io.Writer, s string) {
	var esc []byte
	last := 0
	var html []byte
	lenS := len(s)
	for i, ch := range s {
	for i := 0; i < lenS; i++ {
		switch s[i] {
		switch ch {
		case '\000':
			html = htmlNull
		case ' ':
			esc = escNull
		case '"':
			if visibleSpace {
				html = htmlVisSpace
			} else {
			esc = escQuot
				continue
			}
		case '"':
			html = htmlQuot
		case '\'':
			esc = escApos
		case '&':
			html = htmlAmp
			esc = escAmp
		case '<':
			html = htmlLt
			esc = escLt
		case '>':
			html = htmlGt
		default:
			continue
		}
		io.WriteString(w, s[last:i])
		w.Write(html)
		last = i + 1
	}
	io.WriteString(w, s[last:])
}

			esc = escGt
// HTMLAttrEscape writes to w the escaped HTML equivalent of the given string to be used
// in attributes.
func HTMLAttrEscape(w io.Writer, s string) {
	last := 0
	var html []byte
	lenS := len(s)
	for i := 0; i < lenS; i++ {
		switch s[i] {
		case '\000':
			html = htmlNull
		case '\t':
			esc = escTab
		case '"':
			html = htmlQuot
		case '&':
			html = htmlAmp
		default:
			continue
		}
		io.WriteString(w, s[last:i])
		w.Write(html)
		w.Write(esc)
		last = i + 1
	}
	io.WriteString(w, s[last:])
}

Added strfun/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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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{}

// NewSet creates a new set from the given values.
func NewSet(values ...string) Set {
	s := make(Set, len(values))
	for _, v := range values {
		s.Set(v)
	}
	return s
}

// Set adds the given string to the set.
func (s Set) Set(v string) { s[v] = struct{}{} }

// Has returns true, if given value is in set.
func (s Set) Has(v string) bool { _, found := s[v]; return found }

Changes to strfun/slugify.go.

1
2

3
4

5
6
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

-
+

-
+




+
+
+


-










-
+
-







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package strfun provides some string functions.
package strfun

import (
	"strings"
	"unicode"

	"golang.org/x/text/unicode/norm"
)

// NormalizeWords produces a word list that is normalized for better searching.
func NormalizeWords(s string) []string {
func NormalizeWords(s string) (result []string) {
	result := make([]string, 0, 1)
	word := make([]rune, 0, len(s))
	for _, r := range norm.NFKD.String(s) {
		if unicode.Is(unicode.Diacritic, r) {
			continue
		}
		if unicode.In(r, unicode.Letter, unicode.Number) {
			word = append(word, unicode.ToLower(r))

Changes to strfun/slugify_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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package strfun provides some string functions.
package strfun_test

import (
	"testing"

	"zettelstore.de/z/strfun"
)

func TestSlugify(t *testing.T) {
	t.Parallel()
	tests := []struct{ in, exp string }{
		{"simple test", "simple-test"},
		{"I'm a go developer", "i-m-a-go-developer"},
		{"-!->simple   test<-!-", "simple-test"},
		{"äöüÄÖÜß", "aouaouß"},
		{"\"aèf", "aef"},
		{"a#b", "a-b"},
43
44
45
46
47
48
49

50
51
52
53
54
55
56
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60







+







			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

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
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

-
+

-
+




+
+
+











-
-
-
-
-
-







//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package strfun provides some string functions.
package strfun

import (
	"strings"
	"unicode"
	"unicode/utf8"
)

// TrimSpaceRight returns a slice of the string s, with all trailing white space removed,
// as defined by Unicode.
func TrimSpaceRight(s string) string {
	return strings.TrimRightFunc(s, unicode.IsSpace)
}

// Length returns the number of runes in the given string.
func Length(s string) int {
	return utf8.RuneCountInString(s)
}

// JustifyLeft ensures that the string has a defined length.
func JustifyLeft(s string, maxLen int, pad rune) string {
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58
59
60








39
40
41
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[maxLen-1] = '\u2025'
	}

	var sb strings.Builder
	for _, r := range runes {
		sb.WriteRune(r)
	}
	for i := 0; i < maxLen-len(runes); i++ {
	for range maxLen - len(runes) {
		sb.WriteRune(pad)
	}
	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
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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

-
+

-
+




+
+
+


-








-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+
















+







//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package strfun provides some string functions.
package strfun_test

import (
	"testing"

	"zettelstore.de/z/strfun"
)

func TestTrimSpaceRight(t *testing.T) {
	const space = "\t\v\r\f\n\u0085\u00a0\u2000\u3000"
	testcases := []struct {
		in  string
		exp string
	}{
		{"", ""},
		{"abc", "abc"},
		{" ", ""},
		{space, ""},
		{space + "abc" + space, space + "abc"},
		{" \t\r\n \t\t\r\r\n\n ", ""},
		{" \t\r\n x\t\t\r\r\n\n ", " \t\r\n x"},
		{" \u2000\t\r\n x\t\t\r\r\ny\n \u3000", " \u2000\t\r\n x\t\t\r\r\ny"},
		{"1 \t\r\n2", "1 \t\r\n2"},
		{" x\x80", " x\x80"},
		{" x\xc0", " x\xc0"},
		{"x \xc0\xc0 ", "x \xc0\xc0"},
		{"x \xc0", "x \xc0"},
		{"x \xc0 ", "x \xc0"},
		{"x \xc0\xc0 ", "x \xc0\xc0"},
		{"x ☺\xc0\xc0 ", "x ☺\xc0\xc0"},
		{"x ☺ ", "x ☺"},
	}
	for i, tc := range testcases {
		got := strfun.TrimSpaceRight(tc.in)
		if got != tc.exp {
			t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got)
		}
	}
}

func TestLength(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  string
		exp int
	}{
		{"", 0},
		{"äbc", 3},
	}
	for i, tc := range testcases {
		got := strfun.Length(tc.in)
		if got != tc.exp {
			t.Errorf("%d/%q: expected %v, got %v", i, tc.in, tc.exp, got)
		}
	}
}

func TestJustifyLeft(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  string
		ml  int
		exp string
	}{
		{"", 0, ""},
		{"äbc", 0, ""},
83
84
85
86
87
88
89

90
91
92
93
94
95
96
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69







+







		if got != tc.exp {
			t.Errorf("%d/%q/%d: expected %q, got %q", i, tc.in, tc.ml, tc.exp, got)
		}
	}
}

func TestSplitLines(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  string
		exp []string
	}{
		{"", nil},
		{"\n", nil},
		{"a", []string{"a"}},

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
712
713
714
715
716
717
718
719
720
















































































































































































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// 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/z/strfun"
)

// 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 (e *varNode) node()         {}
func (e *varNode) Type() TagType { return Variable }
func (e *varNode) Name() string  { return e.name }
func (e *varNode) Tags() []Tag   { panic("mustache: Tags on Variable type") }

type sectionNode struct {
	name      string
	inverted  bool
	startline int
	nodes     []node
}

func (e *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 (e *partialNode) node()         {}
func (e *partialNode) Type() TagType { return Partial }
func (e *partialNode) Name() string  { return e.name }
func (e *partialNode) Tags() []Tag   { return nil }

type textNode struct {
	text []byte
}

func (e *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]bool{
	'#': true, '^': true, '/': true, '<': true, '>': true, '=': true, '!': true,
}

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, error) {
	return &partialNode{
		name:   name,
		indent: indent,
		prov:   tmpl.partial,
	}, nil
}

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, err := tmpl.parsePartial(name, textResult.padding)
			if err != nil {
				return err
			}
			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, err := tmpl.parsePartial(name, textResult.padding)
			if err != nil {
				return err
			}
			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 {
				s := fmt.Sprint(val.Interface())
				strfun.HTMLEscape(w, s, false)
			}
		}
	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 (ep *emptyProvider) Get(name 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



























































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// 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) {
	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) {
	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) {
	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) {
	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) {
	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
































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// 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) {
	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, err := os.ReadFile(path)
		if err != nil {
			t.Fatal(err)
		}
		var suite specTestSuite
		err = json.Unmarshal(b, &suite)
		if err != nil {
			t.Fatal(err)
		}
		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/content/blockcomment/20200215204700.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
title: Simple Test

%%%
No render
%%%
%%%{-}
Render
%%%

Deleted testdata/content/cite/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

[@Stern18]{-}

Deleted testdata/content/comment/20200215204700.zettel.

1
2
3
4




-
-
-
-
title: Simple Test

% No comment
%% Comment

Deleted testdata/content/descrlist/20200226122100.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Simple Test

; Zettel
: Paper
: Note
; Zettelkasten
: Slip box

Deleted testdata/content/edit/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

~~delete~~{-}
__insert__{-}
~~kill~~{-}__create__{-}

Deleted testdata/content/footnote/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

Text[^foot]{=sidebar}

Deleted testdata/content/format/20200215204700.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19



















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
title: Simple Test
role: zettel

//italic//
//emph//{-}
**bold**
**strong**{-}
__unterline__
~~strike~~
''monospace''
^^superscript^^
,,subscript,,
""Quotes""
<<Quotation<<
;;small;;
::span::
``code``
++input++
==output==

Deleted testdata/content/format/20201107164400.zettel.

1
2
3
4




-
-
-
-
title: Nested Lang
role: zettel

::""abc""::{lang=fr}

Deleted testdata/content/heading/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

=== First

Deleted testdata/content/hrule/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

---

Deleted testdata/content/image/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

{{abc}}

Deleted testdata/content/link/20200215204700.zettel.

1
2
3
4
5
6
7
8
9
10
11











-
-
-
-
-
-
-
-
-
-
-
title: Simple Test

[[Home|https://zettelstore.de/z]]
[[https://zettelstore.de]]
[[Config|00000000000100]]
[[00000000000100]]
[[Frag|#frag]]
[[#frag]]
[[H|/hosted]]
[[B|//based]]
[[R|../rel]]

Deleted testdata/content/list/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

* Item 1
* Item 2
* Item 3

Deleted testdata/content/list/20200217194800.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
title: Second list

* Item1.1
* Item1.2
* Item1.3

* Item2.1
* Item2.2

Deleted testdata/content/list/20200516105700.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Schachtelliste

* T1
** T2
* T3
** T4
* T5

Deleted testdata/content/literal/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

++input++
``program``
==output==

Deleted testdata/content/mark/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

[!mark]

Deleted testdata/content/paragraph/20200215185900.zettel.

1
2
3
4




-
-
-
-
title: Simple Zettel
tags: #test #simple

This is a zettel for testing.

Deleted testdata/content/paragraph/20200217151800.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
title: Continuation after Paragraph
tags: #test #paragraph

Text Text
*abc

Text Text
* abc

Deleted testdata/content/png/20200512180900.png.

cannot compute difference between binary files

Deleted testdata/content/quoteblock/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

<<<
To be or not to be.
<<< Romeo

Deleted testdata/content/spanblock/20200215204700.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Simple Test

:::
A simple
   span
and much more
:::

Deleted testdata/content/table/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

|c1|c2|c3|

Deleted testdata/content/table/20200618140700.zettel.

1
2
3
4
5
6






-
-
-
-
-
-
title: Testtable

|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3

Deleted testdata/content/verbatim/20200215204700.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Simple Test

```
if __name__ == "main":
  print("Hello, World")
exit(0)
```

Deleted testdata/content/verseblock/20200215204700.zettel.

1
2
3
4
5
6
7
8
9
10
11











-
-
-
-
-
-
-
-
-
-
-
title: Simple Test

"""
A line
  another line
Back

Paragraph

    Spacy  Para
""" Author

Changes to testdata/markdown/README.md.

1

2

1
2
-
+

File `spec.son` is the CommonMark specification test file.
File `spec.json` is the CommonMark specification 0.30 test file.
You can find all versions here: <https://spec.commonmark.org/>.

Changes to testdata/markdown/spec.json.

1
2
3
4
5
6
7


8
9
10
11
12
13
14
15


16
17
18
19
20
21
22
23


24
25
26
27
28
29
30
31


32
33
34
35
36
37
38
39


40
41
42
43
44
45
46
47


48
49
50
51
52
53
54
55


56
57
58
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



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
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
1427
1428
1429
1430
1431



1432
1433
1434
1435
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
1513
1514
1515
1516
1517
1518
1519



1520
1521
1522
1523
1524
1525
1526
1527



1528
1529
1530
1531
1532
1533
1534
1535



1536
1537
1538
1539
1540
1541
1542
1543



1544
1545
1546
1547
1548
1549
1550
1551



1552
1553
1554
1555
1556
1557
1558
1559



1560
1561
1562
1563
1564
1565
1566
1567



1568
1569
1570
1571
1572
1573
1574
1575



1576
1577
1578
1579
1580
1581
1582
1583



1584
1585
1586
1587
1588
1589
1590
1591



1592
1593
1594
1595
1596
1597
1598
1599



1600
1601
1602
1603
1604
1605
1606
1607



1608
1609
1610
1611
1612
1613
1614
1615



1616
1617
1618
1619
1620
1621
1622
1623



1624
1625
1626
1627
1628
1629
1630
1631



1632
1633
1634
1635
1636
1637
1638
1639



1640
1641
1642
1643
1644
1645
1646
1647



1648
1649
1650
1651
1652
1653
1654
1655



1656
1657
1658
1659
1660
1661
1662
1663



1664
1665
1666
1667
1668
1669
1670
1671



1672
1673
1674
1675
1676
1677
1678
1679



1680
1681
1682
1683
1684
1685
1686
1687



1688
1689
1690
1691
1692
1693
1694
1695



1696
1697
1698
1699
1700
1701
1702
1703



1704
1705
1706
1707
1708
1709
1710
1711



1712
1713
1714
1715
1716
1717
1718
1719



1720
1721
1722
1723
1724
1725
1726
1727



1728
1729
1730
1731
1732
1733
1734
1735



1736
1737
1738
1739
1740
1741
1742
1743



1744
1745
1746
1747
1748
1749
1750
1751



1752
1753
1754
1755
1756
1757
1758
1759



1760
1761
1762
1763
1764
1765
1766
1767



1768
1769
1770
1771
1772
1773
1774
1775



1776
1777
1778
1779
1780
1781
1782
1783



1784
1785
1786
1787
1788
1789
1790
1791



1792
1793
1794
1795
1796
1797
1798
1799



1800
1801
1802
1803
1804
1805
1806
1807



1808
1809
1810
1811
1812
1813
1814
1815



1816
1817
1818
1819
1820
1821
1822
1823



1824
1825
1826
1827
1828
1829
1830
1831



1832
1833
1834
1835
1836
1837
1838
1839



1840
1841
1842
1843
1844
1845
1846
1847



1848
1849
1850
1851
1852
1853
1854
1855



1856
1857
1858
1859
1860
1861
1862
1863



1864
1865
1866
1867
1868
1869
1870
1871



1872
1873
1874
1875
1876
1877
1878
1879



1880
1881
1882
1883
1884
1885
1886
1887



1888
1889
1890
1891
1892
1893
1894
1895



1896
1897
1898
1899
1900
1901
1902
1903



1904
1905
1906
1907
1908
1909
1910
1911



1912
1913
1914
1915
1916
1917
1918
1919



1920
1921
1922
1923
1924
1925
1926
1927



1928
1929
1930
1931
1932
1933
1934
1935



1936
1937
1938
1939
1940
1941
1942
1943



1944
1945
1946
1947
1948
1949
1950
1951



1952
1953
1954
1955
1956
1957
1958
1959



1960
1961
1962
1963
1964
1965
1966
1967



1968
1969
1970
1971
1972
1973
1974
1975



1976
1977
1978
1979
1980
1981
1982
1983



1984
1985
1986
1987
1988
1989
1990
1991



1992
1993
1994
1995
1996
1997
1998
1999



2000
2001
2002
2003
2004
2005
2006
2007



2008
2009
2010
2011
2012
2013
2014
2015



2016
2017
2018
2019
2020
2021
2022
2023



2024
2025
2026
2027
2028
2029
2030
2031



2032
2033
2034
2035
2036
2037
2038
2039



2040
2041
2042
2043
2044
2045
2046
2047



2048
2049
2050
2051
2052
2053
2054
2055



2056
2057
2058
2059
2060
2061
2062
2063



2064
2065
2066
2067
2068
2069
2070
2071



2072
2073
2074
2075
2076
2077
2078
2079



2080
2081
2082
2083
2084
2085
2086
2087



2088
2089
2090
2091
2092
2093
2094
2095



2096
2097
2098
2099
2100
2101
2102
2103



2104
2105
2106
2107
2108
2109
2110
2111



2112
2113
2114
2115
2116
2117
2118
2119



2120
2121
2122
2123
2124
2125
2126
2127



2128
2129
2130
2131
2132
2133
2134
2135



2136
2137
2138
2139
2140
2141
2142
2143



2144
2145
2146
2147
2148
2149
2150
2151



2152
2153
2154
2155
2156
2157
2158
2159



2160
2161
2162
2163
2164
2165
2166
2167



2168
2169
2170
2171
2172
2173
2174
2175



2176
2177
2178
2179
2180
2181
2182
2183



2184
2185
2186
2187
2188
2189
2190
2191



2192
2193
2194
2195
2196
2197
2198
2199



2200
2201
2202
2203
2204
2205
2206
2207



2208
2209
2210
2211
2212
2213
2214
2215



2216
2217
2218
2219
2220
2221
2222
2223



2224
2225
2226
2227
2228
2229
2230
2231



2232
2233
2234
2235
2236
2237
2238
2239



2240
2241
2242
2243
2244
2245
2246
2247



2248
2249
2250
2251
2252
2253
2254
2255



2256
2257
2258
2259
2260
2261
2262
2263



2264
2265
2266
2267
2268
2269
2270
2271



2272
2273
2274
2275
2276
2277
2278
2279



2280
2281
2282
2283
2284
2285
2286
2287



2288
2289
2290
2291
2292
2293
2294
2295



2296
2297
2298
2299
2300
2301
2302
2303



2304
2305
2306
2307
2308
2309
2310
2311



2312
2313
2314
2315
2316
2317
2318
2319



2320
2321
2322
2323
2324
2325
2326
2327



2328
2329
2330
2331
2332
2333
2334
2335



2336
2337
2338
2339
2340
2341
2342
2343



2344
2345
2346
2347
2348
2349
2350
2351



2352
2353
2354
2355
2356
2357
2358
2359



2360
2361
2362
2363
2364
2365
2366
2367



2368
2369
2370
2371
2372
2373
2374
2375



2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623


2624
2625
2626
2627
2628
2629
2630
2631


2632
2633
2634
2635
2636
2637
2638
2639


2640
2641
2642
2643
2644
2645
2646
2647


2648
2649
2650
2651
2652
2653
2654
2655


2656
2657
2658
2659
2660
2661
2662
2663


2664
2665
2666
2667
2668
2669
2670
2671


2672
2673
2674
2675
2676
2677
2678
2679


2680
2681
2682
2683
2684
2685
2686
2687


2688
2689
2690
2691
2692
2693
2694
2695


2696
2697
2698
2699
2700
2701
2702
2703


2704
2705
2706
2707
2708
2709
2710
2711


2712
2713
2714
2715
2716
2717
2718
2719


2720
2721
2722
2723
2724
2725
2726
2727


2728
2729
2730
2731
2732
2733
2734
2735


2736
2737
2738
2739
2740
2741
2742
2743


2744
2745
2746
2747
2748
2749
2750
2751


2752
2753
2754
2755
2756
2757
2758
2759


2760
2761
2762
2763
2764
2765
2766
2767


2768
2769
2770
2771
2772
2773
2774
2775


2776
2777
2778
2779
2780
2781
2782
2783


2784
2785
2786
2787
2788
2789
2790
2791


2792
2793
2794
2795
2796
2797
2798
2799


2800
2801
2802
2803
2804
2805
2806
2807


2808
2809
2810
2811
2812
2813
2814
2815


2816
2817
2818
2819
2820
2821
2822
2823


2824
2825
2826
2827
2828
2829
2830
2831


2832
2833
2834
2835
2836
2837
2838
2839


2840
2841
2842
2843
2844
2845
2846
2847


2848
2849
2850
2851
2852
2853
2854
2855


2856
2857
2858
2859
2860
2861
2862
2863


2864
2865
2866
2867
2868
2869
2870
2871


2872
2873
2874
2875
2876
2877
2878
2879


2880
2881
2882
2883
2884
2885
2886
2887


2888
2889
2890
2891
2892
2893
2894
2895


2896
2897
2898
2899
2900
2901
2902
2903


2904
2905
2906
2907
2908
2909
2910
2911


2912
2913
2914
2915
2916
2917
2918
2919


2920
2921
2922
2923
2924
2925
2926
2927


2928
2929
2930
2931
2932
2933
2934
2935


2936
2937
2938
2939
2940
2941
2942
2943


2944
2945
2946
2947
2948
2949
2950
2951


2952
2953
2954
2955
2956
2957
2958
2959


2960
2961
2962
2963
2964
2965
2966
2967


2968
2969
2970
2971
2972
2973
2974
2975


2976
2977
2978
2979
2980
2981
2982
2983


2984
2985
2986
2987
2988
2989
2990
2991


2992
2993
2994
2995
2996
2997
2998
2999


3000
3001
3002
3003
3004
3005
3006
3007


3008
3009
3010
3011
3012
3013
3014
3015


3016
3017
3018
3019
3020
3021
3022
3023


3024
3025
3026
3027
3028
3029
3030
3031


3032
3033
3034
3035
3036
3037
3038
3039


3040
3041
3042
3043
3044
3045
3046
3047


3048
3049
3050
3051
3052
3053
3054
3055


3056
3057
3058
3059
3060
3061
3062
3063


3064
3065
3066
3067
3068
3069
3070
3071


3072
3073
3074
3075
3076
3077
3078
3079


3080
3081
3082
3083
3084
3085
3086
3087


3088
3089
3090
3091
3092
3093
3094
3095


3096
3097
3098
3099
3100
3101
3102
3103


3104
3105
3106
3107
3108
3109
3110
3111


3112
3113
3114
3115
3116
3117
3118
3119


3120
3121
3122
3123
3124
3125
3126
3127


3128
3129
3130
3131
3132
3133
3134
3135


3136
3137
3138
3139
3140
3141
3142
3143


3144
3145
3146
3147
3148
3149
3150
3151


3152
3153
3154
3155
3156
3157
3158
3159


3160
3161
3162
3163
3164
3165
3166
3167


3168
3169
3170
3171
3172
3173
3174
3175


3176
3177
3178
3179
3180
3181
3182
3183


3184
3185
3186
3187
3188
3189
3190
3191


3192
3193
3194
3195
3196
3197
3198
3199


3200
3201
3202
3203
3204
3205
3206
3207


3208
3209
3210
3211
3212
3213
3214
3215


3216
3217
3218
3219
3220
3221
3222
3223


3224
3225
3226
3227
3228
3229
3230
3231


3232
3233
3234
3235
3236
3237
3238
3239


3240
3241
3242
3243
3244
3245
3246
3247


3248
3249
3250
3251
3252
3253
3254
3255


3256
3257
3258
3259
3260
3261
3262
3263


3264
3265
3266
3267
3268
3269
3270
3271


3272
3273
3274
3275
3276
3277
3278
3279


3280
3281
3282
3283
3284
3285
3286
3287


3288
3289
3290
3291
3292
3293
3294
3295


3296
3297
3298
3299
3300
3301
3302
3303


3304
3305
3306
3307
3308
3309
3310
3311


3312
3313
3314
3315
3316
3317
3318
3319


3320
3321
3322
3323
3324
3325
3326
3327


3328
3329
3330
3331
3332
3333
3334
3335


3336
3337
3338
3339
3340
3341
3342
3343


3344
3345
3346
3347
3348
3349
3350
3351


3352
3353
3354
3355
3356
3357
3358
3359


3360
3361
3362
3363
3364
3365
3366
3367


3368
3369
3370
3371
3372
3373
3374
3375


3376
3377
3378
3379
3380
3381
3382
3383


3384
3385
3386
3387
3388
3389
3390
3391


3392
3393
3394
3395
3396
3397
3398
3399


3400
3401
3402
3403
3404
3405
3406
3407


3408
3409
3410
3411
3412
3413
3414
3415


3416
3417
3418
3419
3420
3421
3422
3423


3424
3425
3426
3427
3428
3429
3430
3431


3432
3433
3434
3435
3436
3437
3438
3439


3440
3441
3442
3443
3444
3445
3446
3447


3448
3449
3450
3451
3452
3453
3454
3455


3456
3457
3458
3459
3460
3461
3462
3463


3464
3465
3466
3467
3468
3469
3470
3471


3472
3473
3474
3475
3476
3477
3478
3479


3480
3481
3482
3483
3484
3485
3486
3487


3488
3489
3490
3491
3492
3493
3494
3495


3496
3497
3498
3499
3500
3501
3502
3503


3504
3505
3506
3507
3508
3509
3510
3511


3512
3513
3514
3515
3516
3517
3518
3519


3520
3521
3522
3523
3524
3525
3526
3527


3528
3529
3530
3531
3532
3533
3534
3535


3536
3537
3538
3539
3540
3541
3542
3543


3544
3545
3546
3547
3548
3549
3550
3551


3552
3553
3554
3555
3556
3557
3558
3559


3560
3561
3562
3563
3564
3565
3566
3567


3568
3569
3570
3571
3572
3573
3574
3575


3576
3577
3578
3579
3580
3581
3582
3583


3584
3585
3586
3587
3588
3589
3590
3591


3592
3593
3594
3595
3596
3597
3598
3599


3600
3601
3602
3603
3604
3605
3606
3607


3608
3609
3610
3611
3612
3613
3614
3615


3616
3617
3618
3619
3620
3621
3622
3623


3624
3625
3626
3627
3628
3629
3630
3631


3632
3633
3634
3635
3636
3637
3638
3639


3640
3641
3642
3643
3644
3645
3646
3647


3648
3649
3650
3651
3652
3653
3654
3655


3656
3657
3658
3659
3660
3661
3662
3663


3664
3665
3666
3667
3668
3669
3670
3671


3672
3673
3674
3675
3676
3677
3678
3679


3680
3681
3682
3683
3684
3685
3686
3687


3688
3689
3690
3691
3692
3693
3694
3695


3696
3697
3698
3699
3700
3701
3702
3703


3704
3705
3706
3707
3708
3709
3710
3711


3712
3713
3714
3715
3716
3717
3718
3719


3720
3721
3722
3723
3724
3725
3726
3727


3728
3729
3730
3731
3732
3733
3734
3735


3736
3737
3738
3739
3740
3741
3742
3743


3744
3745
3746
3747
3748
3749
3750
3751


3752
3753
3754
3755
3756
3757
3758
3759


3760
3761
3762
3763
3764
3765
3766
3767


3768
3769
3770
3771
3772
3773
3774
3775


3776
3777
3778
3779
3780
3781
3782
3783


3784
3785
3786
3787
3788
3789
3790
3791


3792
3793
3794
3795
3796
3797
3798
3799


3800
3801
3802
3803
3804
3805
3806
3807


3808
3809
3810
3811
3812
3813
3814
3815


3816
3817
3818
3819
3820
3821
3822
3823


3824
3825
3826
3827
3828
3829
3830
3831


3832
3833
3834
3835
3836
3837
3838
3839


3840
3841
3842
3843
3844
3845
3846
3847


3848
3849
3850
3851
3852
3853
3854
3855










3856
3857
3858
3859
3860
3861
3862
3863



3864
3865
3866
3867
3868
3869
3870
3871











3872
3873
3874
3875
3876
3877
3878
3879



3880
3881
3882
3883
3884
3885
3886
3887



3888
3889
3890
3891
3892
3893
3894
3895



3896
3897
3898
3899
3900
3901
3902
3903



3904
3905
3906
3907
3908
3909
3910
3911



3912
3913
3914
3915
3916
3917
3918
3919



3920
3921
3922
3923
3924
3925
3926
3927



3928
3929
3930
3931
3932
3933
3934
3935



3936
3937
3938
3939
3940
3941
3942
3943











3944
3945
3946
3947
3948
3949
3950
3951



3952
3953
3954
3955
3956
3957
3958
3959



3960
3961
3962
3963
3964
3965
3966
3967



3968
3969
3970
3971
3972
3973
3974
3975



3976
3977
3978
3979
3980
3981
3982
3983



3984
3985
3986
3987
3988
3989
3990
3991



3992
3993
3994
3995
3996
3997
3998
3999



4000
4001
4002
4003
4004
4005
4006
4007



4008
4009
4010
4011
4012
4013
4014
4015



4016
4017
4018
4019
4020
4021
4022
4023



4024
4025
4026
4027
4028
4029
4030
4031



4032
4033
4034
4035
4036
4037
4038
4039



4040
4041
4042
4043
4044
4045
4046
4047



4048
4049
4050
4051
4052
4053
4054
4055



4056
4057
4058
4059
4060
4061
4062
4063



4064
4065
4066
4067
4068
4069
4070
4071



4072
4073
4074
4075
4076
4077
4078
4079



4080
4081
4082
4083
4084
4085
4086
4087



4088
4089
4090
4091
4092
4093
4094
4095



4096
4097
4098
4099
4100
4101
4102
4103



4104
4105
4106
4107
4108
4109
4110
4111



4112
4113
4114
4115
4116
4117
4118
4119



4120
4121
4122
4123
4124
4125
4126
4127



4128
4129
4130
4131
4132
4133
4134
4135



4136
4137
4138
4139
4140
4141
4142
4143



4144
4145
4146
4147
4148
4149
4150
4151



4152
4153
4154
4155
4156
4157
4158
4159



4160
4161
4162
4163
4164
4165
4166
4167



4168
4169
4170
4171
4172
4173
4174
4175



4176
4177
4178
4179
4180
4181
4182
4183



4184
4185
4186
4187
4188
4189
4190
4191



4192
4193
4194
4195
4196
4197
4198
4199



4200
4201
4202
4203
4204
4205
4206
4207



4208
4209
4210
4211
4212
4213
4214
4215



4216
4217
4218
4219
4220
4221
4222
4223



4224
4225
4226
4227
4228
4229
4230
4231



4232
4233
4234
4235
4236
4237
4238
4239



4240
4241
4242
4243
4244
4245
4246
4247





4248
4249
4250
4251
4252
4253
4254
4255



4256
4257
4258
4259
4260
4261
4262
4263



4264
4265
4266
4267
4268
4269
4270
4271



4272
4273
4274
4275
4276
4277
4278
4279



4280
4281
4282
4283
4284
4285
4286
4287





4288
4289
4290
4291
4292
4293
4294
4295



4296
4297
4298
4299
4300
4301
4302
4303



4304
4305
4306
4307
4308
4309
4310
4311



4312
4313
4314
4315
4316
4317
4318
4319



4320
4321
4322
4323
4324
4325
4326
4327



4328
4329
4330
4331
4332
4333
4334
4335



4336
4337
4338
4339
4340
4341
4342
4343



4344
4345
4346
4347
4348
4349
4350
4351



4352
4353
4354
4355
4356
4357
4358
4359



4360
4361
4362
4363
4364
4365
4366
4367



4368
4369
4370
4371
4372
4373
4374
4375



4376
4377
4378
4379
4380
4381
4382
4383



4384
4385
4386
4387
4388
4389
4390
4391



4392
4393
4394
4395
4396
4397
4398
4399



4400
4401
4402
4403
4404
4405
4406
4407



4408
4409
4410
4411
4412
4413
4414
4415



4416
4417
4418
4419
4420
4421
4422
4423



4424
4425
4426
4427
4428
4429
4430
4431



4432
4433
4434
4435
4436
4437
4438
4439



4440
4441
4442
4443
4444
4445
4446
4447



4448
4449
4450
4451
4452
4453
4454
4455



4456
4457
4458
4459
4460
4461
4462
4463



4464
4465
4466
4467
4468
4469
4470
4471



4472
4473
4474
4475
4476
4477
4478
4479



4480
4481
4482
4483
4484
4485
4486
4487



4488
4489
4490
4491
4492
4493
4494
4495



4496
4497
4498
4499
4500
4501
4502
4503



4504
4505
4506
4507
4508
4509
4510
4511



4512
4513
4514
4515
4516
4517
4518
4519



4520
4521
4522
4523
4524
4525
4526
4527



4528
4529
4530
4531
4532
4533
4534
4535



4536
4537
4538
4539
4540
4541
4542
4543



4544
4545
4546
4547
4548
4549
4550
4551



4552
4553
4554
4555
4556
4557
4558
4559



4560
4561
4562
4563
4564
4565
4566
4567



4568
4569
4570
4571
4572
4573
4574
4575



4576
4577
4578
4579
4580
4581
4582
4583



4584
4585
4586
4587
4588
4589
4590
4591



4592
4593
4594
4595
4596
4597
4598
4599



4600
4601
4602
4603
4604
4605
4606
4607



4608
4609
4610
4611
4612
4613
4614
4615



4616
4617
4618
4619
4620
4621
4622
4623



4624
4625
4626
4627
4628
4629
4630
4631



4632
4633
4634
4635
4636
4637
4638
4639



4640
4641
4642
4643
4644
4645
4646
4647



4648
4649
4650
4651
4652
4653
4654
4655



4656
4657
4658
4659
4660
4661
4662
4663



4664
4665
4666
4667
4668
4669
4670
4671



4672
4673
4674
4675
4676
4677
4678
4679



4680
4681
4682
4683
4684
4685
4686
4687



4688
4689
4690
4691
4692
4693
4694
4695



4696
4697
4698
4699
4700
4701
4702
4703



4704
4705
4706
4707
4708
4709
4710
4711



4712
4713
4714
4715
4716
4717
4718
4719



4720
4721
4722
4723
4724
4725
4726
4727



4728
4729
4730
4731
4732
4733
4734
4735



4736
4737
4738
4739
4740
4741
4742
4743



4744
4745
4746
4747
4748
4749
4750
4751



4752
4753
4754
4755
4756
4757
4758
4759



4760
4761
4762
4763
4764
4765
4766
4767



4768
4769
4770
4771
4772
4773
4774
4775



4776
4777
4778
4779
4780
4781
4782
4783



4784
4785
4786
4787
4788
4789
4790
4791



4792
4793
4794
4795
4796
4797
4798
4799



4800
4801
4802
4803
4804
4805
4806
4807



4808
4809
4810
4811
4812
4813
4814
4815



4816
4817
4818
4819
4820
4821
4822
4823



4824
4825
4826
4827
4828
4829
4830
4831



4832
4833
4834
4835
4836
4837
4838
4839



4840
4841
4842
4843
4844
4845
4846
4847



4848
4849
4850
4851
4852
4853
4854
4855



4856
4857
4858
4859
4860
4861
4862
4863



4864
4865
4866
4867
4868
4869
4870
4871



4872
4873
4874
4875
4876
4877
4878
4879



4880
4881
4882
4883
4884
4885
4886
4887



4888
4889
4890
4891
4892
4893
4894
4895



4896
4897
4898
4899
4900
4901
4902
4903



4904
4905
4906
4907
4908
4909
4910
4911



4912
4913
4914
4915
4916
4917
4918
4919



4920
4921
4922
4923
4924
4925
4926
4927



4928
4929
4930
4931
4932
4933
4934
4935



4936
4937
4938
4939
4940
4941
4942
4943



4944
4945
4946
4947
4948
4949
4950
4951



4952
4953
4954
4955
4956
4957
4958
4959



4960
4961
4962
4963
4964
4965
4966
4967



4968
4969
4970
4971
4972
4973
4974
4975



4976
4977
4978
4979
4980
4981
4982
4983



4984
4985
4986
4987
4988
4989
4990
4991



4992
4993
4994
4995
4996
4997
4998
4999



5000
5001
5002
5003
5004
5005
5006
5007



5008
5009
5010
5011
5012
5013
5014
5015



5016
5017
5018
5019
5020
5021
5022
5023



5024
5025
5026
5027
5028
5029
5030
5031



5032
5033
5034
5035
5036
5037
5038
5039



5040
5041
5042
5043
5044
5045
5046
5047



5048
5049
5050
5051
5052
5053
5054
5055



5056
5057
5058
5059
5060
5061
5062
5063



5064
5065
5066
5067
5068
5069
5070
5071



5072
5073
5074
5075
5076
5077
5078
5079



5080
5081
5082
5083
5084
5085
5086
5087



5088
5089
5090
5091
5092
5093
5094
5095





5096
5097
5098
5099
5100
5101
5102
5103



5104
5105
5106
5107
5108
5109
5110
5111



5112
5113
5114
5115
5116
5117
5118
5119



5120
5121
5122
5123
5124
5125
5126
5127



5128
5129
5130
5131
5132
5133
5134
5135



5136
5137
5138
5139
5140
5141
5142
5143



5144
5145
5146
5147
5148
5149
5150
5151



5152
5153
5154
5155
5156
5157
5158
5159



5160
5161
5162
5163
5164
5165
5166
5167



5168
5169
5170
5171
5172
5173
5174
5175



5176
5177
5178
5179
5180
5181
5182
5183



5184
5185
5186
5187
5188
5189
5190
5191



5192
5193
5194
1
2
3
4
5


6
7
8
9
10
11
12
13


14
15
16
17
18
19
20
21


22
23
24
25
26
27
28
29


30
31
32
33
34
35
36
37


38
39
40
41
42
43
44
45


46
47
48
49
50
51
52
53


54
55
56
57
58
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
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



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
1427
1428



1429
1430
1431
1432
1433
1434
1435
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
1513
1514
1515
1516



1517
1518
1519
1520
1521
1522
1523
1524



1525
1526
1527
1528
1529
1530
1531
1532



1533
1534
1535
1536
1537
1538
1539
1540



1541
1542
1543
1544
1545
1546
1547
1548



1549
1550
1551
1552
1553
1554
1555
1556



1557
1558
1559
1560
1561
1562
1563
1564



1565
1566
1567
1568
1569
1570
1571
1572



1573
1574
1575
1576
1577
1578
1579
1580



1581
1582
1583
1584
1585
1586
1587
1588



1589
1590
1591
1592
1593
1594
1595
1596



1597
1598
1599
1600
1601
1602
1603
1604



1605
1606
1607
1608
1609
1610
1611
1612



1613
1614
1615
1616
1617
1618
1619
1620



1621
1622
1623
1624
1625
1626
1627
1628



1629
1630
1631
1632
1633
1634
1635
1636



1637
1638
1639
1640
1641
1642
1643
1644



1645
1646
1647
1648
1649
1650
1651
1652



1653
1654
1655
1656
1657
1658
1659
1660



1661
1662
1663
1664
1665
1666
1667
1668



1669
1670
1671
1672
1673
1674
1675
1676



1677
1678
1679
1680
1681
1682
1683
1684



1685
1686
1687
1688
1689
1690
1691
1692



1693
1694
1695
1696
1697
1698
1699
1700



1701
1702
1703
1704
1705
1706
1707
1708



1709
1710
1711
1712
1713
1714
1715
1716



1717
1718
1719
1720
1721
1722
1723
1724



1725
1726
1727
1728
1729
1730
1731
1732



1733
1734
1735
1736
1737
1738
1739
1740



1741
1742
1743








1744
1745
1746
1747
1748



1749
1750
1751
1752
1753
1754
1755
1756



1757
1758
1759
1760
1761
1762
1763
1764



1765
1766
1767
1768
1769
1770
1771
1772



1773
1774
1775
1776
1777
1778
1779
1780



1781
1782
1783
1784
1785
1786
1787
1788



1789
1790
1791
1792
1793
1794
1795
1796



1797
1798
1799
1800
1801
1802
1803
1804



1805
1806
1807
1808
1809
1810
1811
1812



1813
1814
1815
1816
1817
1818
1819
1820



1821
1822
1823
1824
1825
1826
1827
1828



1829
1830
1831
1832
1833
1834
1835
1836



1837
1838
1839
1840
1841
1842
1843
1844



1845
1846
1847
1848
1849
1850
1851
1852



1853
1854
1855
1856
1857
1858
1859
1860



1861
1862
1863
1864
1865
1866
1867
1868



1869
1870
1871
1872
1873
1874
1875
1876



1877
1878
1879
1880
1881
1882
1883
1884



1885
1886
1887
1888
1889
1890
1891
1892



1893
1894
1895
1896
1897
1898
1899
1900



1901
1902
1903
1904
1905
1906
1907
1908



1909
1910
1911
1912
1913
1914
1915
1916



1917
1918
1919
1920
1921
1922
1923
1924



1925
1926
1927
1928
1929
1930
1931
1932



1933
1934
1935
1936
1937
1938
1939
1940



1941
1942
1943
1944
1945
1946
1947
1948



1949
1950
1951
1952
1953
1954
1955
1956



1957
1958
1959
1960
1961
1962
1963
1964



1965
1966
1967
1968
1969
1970
1971
1972



1973
1974
1975
1976
1977
1978
1979
1980



1981
1982
1983
1984
1985
1986
1987
1988



1989
1990
1991
1992
1993
1994
1995
1996



1997
1998
1999
2000
2001
2002
2003
2004



2005
2006
2007
2008
2009
2010
2011
2012



2013
2014
2015
2016
2017
2018
2019
2020



2021
2022
2023
2024
2025
2026
2027
2028



2029
2030
2031
2032
2033
2034
2035
2036



2037
2038
2039
2040
2041
2042
2043
2044



2045
2046
2047
2048
2049
2050
2051
2052



2053
2054
2055
2056
2057
2058
2059
2060



2061
2062
2063
2064
2065
2066
2067
2068



2069
2070
2071
2072
2073
2074
2075
2076



2077
2078
2079
2080
2081
2082
2083
2084



2085
2086
2087
2088
2089
2090
2091
2092



2093
2094
2095
2096
2097
2098
2099
2100



2101
2102
2103
2104
2105
2106
2107
2108



2109
2110
2111
2112
2113
2114
2115
2116



2117
2118
2119
2120
2121
2122
2123
2124



2125
2126
2127
2128
2129
2130
2131
2132



2133
2134
2135
2136
2137
2138
2139
2140



2141
2142
2143
2144
2145
2146
2147
2148



2149
2150
2151
2152
2153
2154
2155
2156



2157
2158
2159
2160
2161
2162
2163
2164



2165
2166
2167
2168
2169
2170
2171
2172



2173
2174
2175
2176
2177
2178
2179
2180



2181
2182
2183
2184
2185
2186
2187
2188



2189
2190
2191
2192
2193
2194
2195
2196



2197
2198
2199
2200
2201
2202
2203
2204



2205
2206
2207
2208
2209
2210
2211
2212



2213
2214
2215
2216
2217
2218
2219
2220



2221
2222
2223
2224
2225
2226
2227
2228



2229
2230
2231
2232
2233
2234
2235
2236



2237
2238
2239
2240
2241
2242
2243
2244



2245
2246
2247
2248
2249
2250
2251
2252



2253
2254
2255
2256
2257
2258
2259
2260



2261
2262
2263
2264
2265
2266
2267
2268



2269
2270
2271
2272
2273
2274
2275
2276



2277
2278
2279
2280
2281
2282
2283
2284



2285
2286
2287
2288
2289
2290
2291
2292



2293
2294
2295
2296
2297
2298
2299
2300



2301
2302
2303
2304
2305
2306
2307
2308



2309
2310
2311
2312
2313
2314
2315
2316



2317
2318
2319
2320
2321
2322
2323
2324



2325
2326
2327
2328
2329
2330
2331
2332



2333
2334
2335
2336
2337
2338
2339
2340



2341
2342
2343
2344
2345
2346
2347
2348



2349
2350
2351
2352
2353
2354
2355
2356



2357
2358
2359
2360
2361
2362
2363
2364



2365
2366
2367
2368
2369
2370
2371
2372



2373
2374
2375
2376
2377
2378
2379
2380



2381
2382
2383
2384
2385
2386
2387
2388



2389
2390
2391
2392
2393
2394
2395
2396



2397
2398
2399
2400
2401
2402
2403
2404



2405
2406
2407
2408
2409
2410
2411
2412



2413
2414
2415
2416
2417
2418
2419
2420



2421
2422
2423
2424
2425
2426
2427
2428



2429
2430
2431
2432
2433
2434
2435
2436



2437
2438
2439
2440
2441
2442
2443
2444



2445
2446
2447
2448
2449
2450
2451
2452



2453
2454
2455
2456
2457
2458
2459
2460



2461
2462
2463
2464
2465
2466
2467
2468



2469
2470
2471
2472
2473
2474
2475
2476



2477
2478
2479
2480
2481
2482
2483
2484



2485
2486
2487
2488
2489
2490
2491
2492



2493
2494
2495
2496
2497
2498
2499
2500



2501
2502
2503
2504
2505
2506
2507
2508



2509
2510
2511
2512
2513
2514
2515
2516



2517
2518
2519
2520
2521
2522
2523
2524



2525
2526
2527
2528
2529
2530
2531
2532



2533
2534
2535
2536
2537
2538
2539
2540



2541
2542
2543
2544
2545
2546
2547
2548



2549
2550
2551
2552
2553
2554
2555
2556



2557
2558
2559
2560
2561
2562
2563
2564



2565
2566
2567
2568
2569
2570
2571
2572



2573
2574
2575
2576
2577
2578
2579
2580



2581
2582
2583
2584
2585
2586
2587
2588



2589
2590
2591
2592
2593
2594
2595
2596



2597
2598
2599
2600
2601
2602
2603
2604



2605
2606
2607
2608
2609
2610
2611
2612



2613
2614
2615
2616
















































































































































































































































2617
2618
2619
2620
2621


2622
2623
2624
2625
2626
2627
2628
2629


2630
2631
2632
2633
2634
2635
2636
2637


2638
2639
2640
2641
2642
2643
2644
2645


2646
2647
2648
2649
2650
2651
2652
2653


2654
2655
2656
2657
2658
2659
2660
2661


2662
2663
2664
2665
2666
2667
2668
2669


2670
2671
2672
2673
2674
2675
2676
2677


2678
2679
2680
2681
2682
2683
2684
2685


2686
2687
2688
2689
2690
2691
2692
2693


2694
2695
2696
2697
2698
2699
2700
2701


2702
2703
2704
2705
2706
2707
2708
2709


2710
2711
2712
2713
2714
2715
2716
2717


2718
2719
2720
2721
2722
2723
2724
2725


2726
2727
2728
2729
2730
2731
2732
2733


2734
2735
2736
2737
2738
2739
2740
2741


2742
2743
2744
2745
2746
2747
2748
2749


2750
2751
2752
2753
2754
2755
2756
2757


2758
2759
2760
2761
2762
2763
2764
2765


2766
2767
2768
2769
2770
2771
2772
2773


2774
2775
2776
2777
2778
2779
2780
2781


2782
2783
2784
2785
2786
2787
2788
2789


2790
2791
2792
2793
2794
2795
2796
2797


2798
2799
2800
2801
2802
2803
2804
2805


2806
2807
2808
2809
2810
2811
2812
2813


2814
2815
2816
2817
2818
2819
2820
2821


2822
2823
2824
2825
2826
2827
2828
2829


2830
2831
2832
2833
2834
2835
2836
2837


2838
2839
2840
2841
2842
2843
2844
2845


2846
2847
2848
2849
2850
2851
2852
2853


2854
2855
2856
2857
2858
2859
2860
2861


2862
2863
2864
2865
2866
2867
2868
2869


2870
2871
2872
2873
2874
2875
2876
2877


2878
2879
2880
2881
2882
2883
2884
2885


2886
2887
2888
2889
2890
2891
2892
2893


2894
2895
2896
2897
2898
2899
2900
2901


2902
2903
2904
2905
2906
2907
2908
2909


2910
2911
2912
2913
2914
2915
2916
2917


2918
2919
2920
2921
2922
2923
2924
2925


2926
2927
2928
2929
2930
2931
2932
2933


2934
2935
2936
2937
2938
2939
2940
2941


2942
2943
2944
2945
2946
2947
2948
2949


2950
2951
2952
2953
2954
2955
2956
2957


2958
2959
2960
2961
2962
2963
2964
2965


2966
2967
2968
2969
2970
2971
2972
2973


2974
2975
2976
2977
2978
2979
2980
2981


2982
2983
2984
2985
2986
2987
2988
2989


2990
2991
2992
2993
2994
2995
2996
2997


2998
2999
3000
3001
3002
3003
3004
3005


3006
3007
3008
3009
3010
3011
3012
3013


3014
3015
3016
3017
3018
3019
3020
3021


3022
3023
3024
3025
3026
3027
3028
3029


3030
3031
3032
3033
3034
3035
3036
3037


3038
3039
3040
3041
3042
3043
3044
3045


3046
3047
3048
3049
3050
3051
3052
3053


3054
3055
3056
3057
3058
3059
3060
3061


3062
3063
3064
3065
3066
3067
3068
3069


3070
3071
3072
3073
3074
3075
3076
3077


3078
3079
3080
3081
3082
3083
3084
3085


3086
3087
3088
3089
3090
3091
3092
3093


3094
3095
3096
3097
3098
3099
3100
3101


3102
3103
3104
3105
3106
3107
3108
3109


3110
3111
3112
3113
3114
3115
3116
3117


3118
3119
3120
3121
3122
3123
3124
3125


3126
3127
3128
3129
3130
3131
3132
3133


3134
3135
3136
3137
3138
3139
3140
3141


3142
3143
3144
3145
3146
3147
3148
3149


3150
3151
3152
3153
3154
3155
3156
3157


3158
3159
3160
3161
3162
3163
3164
3165


3166
3167
3168
3169
3170
3171
3172
3173


3174
3175
3176
3177
3178
3179
3180
3181


3182
3183
3184
3185
3186
3187
3188
3189


3190
3191
3192
3193
3194
3195
3196
3197


3198
3199
3200
3201
3202
3203
3204
3205


3206
3207
3208
3209
3210
3211
3212
3213


3214
3215
3216
3217
3218
3219
3220
3221


3222
3223
3224
3225
3226
3227
3228
3229


3230
3231
3232
3233
3234
3235
3236
3237


3238
3239
3240
3241
3242
3243
3244
3245


3246
3247
3248
3249
3250
3251
3252
3253


3254
3255
3256
3257
3258
3259
3260
3261


3262
3263
3264
3265
3266
3267
3268
3269


3270
3271
3272
3273
3274
3275
3276
3277


3278
3279
3280
3281
3282
3283
3284
3285


3286
3287
3288
3289
3290
3291
3292
3293


3294
3295
3296
3297
3298
3299
3300
3301


3302
3303
3304
3305
3306
3307
3308
3309


3310
3311
3312
3313
3314
3315
3316
3317


3318
3319
3320
3321
3322
3323
3324
3325


3326
3327
3328
3329
3330
3331
3332
3333


3334
3335
3336
3337
3338
3339
3340
3341


3342
3343
3344
3345
3346
3347
3348
3349


3350
3351
3352
3353
3354
3355
3356
3357


3358
3359
3360
3361
3362
3363
3364
3365


3366
3367
3368
3369
3370
3371
3372
3373


3374
3375
3376
3377
3378
3379
3380
3381


3382
3383
3384
3385
3386
3387
3388
3389


3390
3391
3392
3393
3394
3395
3396
3397


3398
3399
3400
3401
3402
3403
3404
3405


3406
3407
3408
3409
3410
3411
3412
3413


3414
3415
3416
3417
3418
3419
3420
3421


3422
3423
3424
3425
3426
3427
3428
3429


3430
3431
3432
3433
3434
3435
3436
3437


3438
3439
3440
3441
3442
3443
3444
3445


3446
3447
3448
3449
3450
3451
3452
3453


3454
3455
3456
3457
3458
3459
3460
3461


3462
3463
3464
3465
3466
3467
3468
3469


3470
3471
3472
3473
3474
3475
3476
3477


3478
3479
3480
3481
3482
3483
3484
3485


3486
3487
3488
3489
3490
3491
3492
3493


3494
3495
3496
3497
3498
3499
3500
3501


3502
3503
3504
3505
3506
3507
3508
3509


3510
3511
3512
3513
3514
3515
3516
3517


3518
3519
3520
3521
3522
3523
3524
3525


3526
3527
3528
3529
3530
3531
3532
3533


3534
3535
3536
3537
3538
3539
3540
3541


3542
3543
3544
3545
3546
3547
3548
3549


3550
3551
3552
3553
3554
3555
3556
3557


3558
3559
3560
3561
3562
3563
3564
3565


3566
3567
3568
3569
3570
3571
3572
3573


3574
3575
3576
3577
3578
3579
3580
3581


3582
3583
3584
3585
3586
3587
3588
3589


3590
3591
3592
3593
3594
3595
3596
3597


3598
3599
3600
3601
3602
3603
3604
3605


3606
3607
3608
3609
3610
3611
3612
3613


3614
3615
3616
3617
3618
3619
3620
3621


3622
3623
3624
3625
3626
3627
3628
3629


3630
3631
3632
3633
3634
3635
3636
3637


3638
3639
3640
3641
3642
3643
3644
3645


3646
3647
3648
3649
3650
3651
3652
3653


3654
3655
3656
3657
3658
3659
3660
3661


3662
3663
3664
3665
3666
3667
3668
3669


3670
3671
3672
3673
3674
3675
3676
3677


3678
3679
3680
3681
3682
3683
3684
3685


3686
3687
3688
3689
3690
3691
3692
3693


3694
3695
3696
3697
3698
3699
3700
3701


3702
3703
3704
3705
3706
3707
3708
3709


3710
3711
3712
3713
3714
3715
3716
3717


3718
3719
3720
3721
3722
3723
3724
3725


3726
3727
3728
3729
3730
3731
3732
3733


3734
3735
3736
3737
3738
3739
3740
3741


3742
3743
3744
3745
3746
3747
3748
3749


3750
3751
3752
3753
3754
3755
3756
3757


3758
3759
3760
3761
3762
3763
3764
3765


3766
3767
3768
3769
3770
3771
3772
3773


3774
3775
3776
3777
3778
3779
3780
3781


3782
3783
3784
3785
3786
3787
3788
3789


3790
3791
3792
3793
3794
3795
3796
3797


3798
3799
3800
3801
3802
3803
3804
3805


3806
3807
3808
3809
3810
3811
3812
3813


3814
3815
3816
3817
3818
3819
3820
3821


3822
3823
3824
3825
3826
3827
3828
3829


3830
3831
3832
3833
3834
3835
3836
3837


3838
3839
3840
3841
3842
3843
3844
3845


3846
3847
3848
3849
3850
3851
3852
3853


3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868



3869
3870
3871
3872
3873
3874
3875
3876



3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892



3893
3894
3895
3896
3897
3898
3899
3900



3901
3902
3903
3904
3905
3906
3907
3908



3909
3910
3911
3912
3913
3914
3915
3916



3917
3918
3919
3920
3921
3922
3923
3924



3925
3926
3927
3928
3929
3930
3931
3932



3933
3934
3935
3936
3937
3938
3939
3940



3941
3942
3943
3944
3945
3946
3947
3948



3949
3950
3951
3952
3953
3954
3955
3956



3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972



3973
3974
3975
3976
3977
3978
3979
3980



3981
3982
3983
3984
3985
3986
3987
3988



3989
3990
3991
3992
3993
3994
3995
3996



3997
3998
3999
4000
4001
4002
4003
4004



4005
4006
4007
4008
4009
4010
4011
4012



4013
4014
4015
4016
4017
4018
4019
4020



4021
4022
4023
4024
4025
4026
4027
4028



4029
4030
4031
4032
4033
4034
4035
4036



4037
4038
4039
4040
4041
4042
4043
4044



4045
4046
4047
4048
4049
4050
4051
4052



4053
4054
4055
4056
4057
4058
4059
4060



4061
4062
4063
4064
4065
4066
4067
4068



4069
4070
4071
4072
4073
4074
4075
4076



4077
4078
4079
4080
4081
4082
4083
4084



4085
4086
4087
4088
4089
4090
4091
4092



4093
4094
4095
4096
4097
4098
4099
4100



4101
4102
4103
4104
4105
4106
4107
4108



4109
4110
4111
4112
4113
4114
4115
4116



4117
4118
4119
4120
4121
4122
4123
4124



4125
4126
4127
4128
4129
4130
4131
4132



4133
4134
4135
4136
4137
4138
4139
4140



4141
4142
4143
4144
4145
4146
4147
4148



4149
4150
4151
4152
4153
4154
4155
4156



4157
4158
4159
4160
4161
4162
4163
4164



4165
4166
4167
4168
4169
4170
4171
4172



4173
4174
4175
4176
4177
4178
4179
4180



4181
4182
4183
4184
4185
4186
4187
4188



4189
4190
4191
4192
4193
4194
4195
4196



4197
4198
4199
4200
4201
4202
4203
4204



4205
4206
4207
4208
4209
4210
4211
4212



4213
4214
4215
4216
4217
4218
4219
4220



4221
4222
4223
4224
4225
4226
4227
4228



4229
4230
4231
4232
4233
4234
4235
4236



4237
4238
4239
4240
4241
4242
4243
4244



4245
4246
4247
4248
4249
4250
4251
4252



4253
4254
4255
4256
4257
4258
4259
4260



4261
4262
4263
4264
4265
4266





4267
4268
4269
4270
4271
4272
4273
4274
4275
4276



4277
4278
4279
4280
4281
4282
4283
4284



4285
4286
4287
4288
4289
4290
4291
4292



4293
4294
4295
4296
4297
4298
4299
4300



4301
4302
4303
4304
4305
4306





4307
4308
4309
4310
4311
4312
4313
4314
4315
4316



4317
4318
4319
4320
4321
4322
4323
4324



4325
4326
4327
4328
4329
4330
4331
4332



4333
4334
4335
4336
4337
4338
4339
4340



4341
4342
4343
4344
4345
4346
4347
4348



4349
4350
4351
4352
4353
4354
4355
4356



4357
4358
4359
4360
4361
4362
4363
4364



4365
4366
4367
4368
4369
4370
4371
4372



4373
4374
4375
4376
4377
4378
4379
4380



4381
4382
4383
4384
4385
4386
4387
4388



4389
4390
4391
4392
4393
4394
4395
4396



4397
4398
4399
4400
4401
4402
4403
4404



4405
4406
4407
4408
4409
4410
4411
4412



4413
4414
4415
4416
4417
4418
4419
4420



4421
4422
4423
4424
4425
4426
4427
4428



4429
4430
4431
4432
4433
4434
4435
4436



4437
4438
4439
4440
4441
4442
4443
4444



4445
4446
4447
4448
4449
4450
4451
4452



4453
4454
4455
4456
4457
4458
4459
4460



4461
4462
4463
4464
4465
4466
4467
4468



4469
4470
4471
4472
4473
4474
4475
4476



4477
4478
4479
4480
4481
4482
4483
4484



4485
4486
4487
4488
4489
4490
4491
4492



4493
4494
4495
4496
4497
4498
4499
4500



4501
4502
4503
4504
4505
4506
4507
4508



4509
4510
4511
4512
4513
4514
4515
4516



4517
4518
4519
4520
4521
4522
4523
4524



4525
4526
4527
4528
4529
4530
4531
4532



4533
4534
4535
4536
4537
4538
4539
4540



4541
4542
4543
4544
4545
4546
4547
4548



4549
4550
4551
4552
4553
4554
4555
4556



4557
4558
4559
4560
4561
4562
4563
4564



4565
4566
4567
4568
4569
4570
4571
4572



4573
4574
4575
4576
4577
4578
4579
4580



4581
4582
4583
4584
4585
4586
4587
4588



4589
4590
4591
4592
4593
4594
4595
4596



4597
4598
4599
4600
4601
4602
4603
4604



4605
4606
4607
4608
4609
4610
4611
4612



4613
4614
4615
4616
4617
4618
4619
4620



4621
4622
4623
4624
4625
4626
4627
4628



4629
4630
4631
4632
4633
4634
4635
4636



4637
4638
4639
4640
4641
4642
4643
4644



4645
4646
4647
4648
4649
4650
4651
4652



4653
4654
4655
4656
4657
4658
4659
4660



4661
4662
4663
4664
4665
4666
4667
4668



4669
4670
4671
4672
4673
4674
4675
4676



4677
4678
4679
4680
4681
4682
4683
4684



4685
4686
4687
4688
4689
4690
4691
4692



4693
4694
4695
4696
4697
4698
4699
4700



4701
4702
4703
4704
4705
4706
4707
4708



4709
4710
4711
4712
4713
4714
4715
4716



4717
4718
4719
4720
4721
4722
4723
4724



4725
4726
4727
4728
4729
4730
4731
4732



4733
4734
4735
4736
4737
4738
4739
4740



4741
4742
4743
4744
4745
4746
4747
4748



4749
4750
4751
4752
4753
4754
4755
4756



4757
4758
4759
4760
4761
4762
4763
4764



4765
4766
4767
4768
4769
4770
4771
4772



4773
4774
4775
4776
4777
4778
4779
4780



4781
4782
4783
4784
4785
4786
4787
4788



4789
4790
4791
4792
4793
4794
4795
4796



4797
4798
4799
4800
4801
4802
4803
4804



4805
4806
4807
4808
4809
4810
4811
4812



4813
4814
4815
4816
4817
4818
4819
4820



4821
4822
4823
4824
4825
4826
4827
4828



4829
4830
4831
4832
4833
4834
4835
4836



4837
4838
4839
4840
4841
4842
4843
4844



4845
4846
4847
4848
4849
4850
4851
4852



4853
4854
4855
4856
4857
4858
4859
4860



4861
4862
4863
4864
4865
4866
4867
4868



4869
4870
4871
4872
4873
4874
4875
4876



4877
4878
4879
4880
4881
4882
4883
4884



4885
4886
4887
4888
4889
4890
4891
4892



4893
4894
4895
4896
4897
4898
4899
4900



4901
4902
4903
4904
4905
4906
4907
4908



4909
4910
4911
4912
4913
4914
4915
4916



4917
4918
4919
4920
4921
4922
4923
4924



4925
4926
4927
4928
4929
4930
4931
4932



4933
4934
4935
4936
4937
4938
4939
4940



4941
4942
4943
4944
4945
4946
4947
4948



4949
4950
4951
4952
4953
4954
4955
4956



4957
4958
4959
4960
4961
4962
4963
4964



4965
4966
4967
4968
4969
4970
4971
4972



4973
4974
4975
4976
4977
4978
4979
4980



4981
4982
4983
4984
4985
4986
4987
4988



4989
4990
4991
4992
4993
4994
4995
4996



4997
4998
4999
5000
5001
5002
5003
5004



5005
5006
5007
5008
5009
5010
5011
5012



5013
5014
5015
5016
5017
5018
5019
5020



5021
5022
5023
5024
5025
5026
5027
5028



5029
5030
5031
5032
5033
5034
5035
5036



5037
5038
5039
5040
5041
5042
5043
5044



5045
5046
5047
5048
5049
5050
5051
5052



5053
5054
5055
5056
5057
5058
5059
5060



5061
5062
5063
5064
5065
5066
5067
5068



5069
5070
5071
5072
5073
5074
5075
5076



5077
5078
5079
5080
5081
5082
5083
5084



5085
5086
5087
5088
5089
5090
5091
5092



5093
5094
5095
5096
5097
5098
5099
5100



5101
5102
5103
5104
5105
5106
5107
5108



5109
5110
5111
5112
5113
5114





5115
5116
5117
5118
5119
5120
5121
5122
5123
5124



5125
5126
5127
5128
5129
5130
5131
5132



5133
5134
5135
5136
5137
5138
5139
5140



5141
5142
5143
5144
5145
5146
5147
5148



5149
5150
5151
5152
5153
5154
5155
5156



5157
5158
5159
5160
5161
5162
5163
5164



5165
5166
5167
5168
5169
5170
5171
5172



5173
5174
5175
5176
5177
5178
5179
5180



5181
5182
5183
5184
5185
5186
5187
5188



5189
5190
5191
5192
5193
5194
5195
5196



5197
5198
5199
5200
5201
5202
5203
5204



5205
5206
5207
5208
5209
5210
5211
5212



5213
5214
5215
5216
5217
5218





-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+
+
+
+
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+
-
-
-
-
-
-
-
-





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-





-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+
+
+
+
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+
+
+
+
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+
+
+
+
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



-
-
-
-
-
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



-
-
-
-
-
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



-
-
-
-
-
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



[
  {
    "markdown": "\tfoo\tbaz\t\tbim\n",
    "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n",
    "example": 1,
    "start_line": 352,
    "end_line": 357,
    "start_line": 356,
    "end_line": 361,
    "section": "Tabs"
  },
  {
    "markdown": "  \tfoo\tbaz\t\tbim\n",
    "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n",
    "example": 2,
    "start_line": 359,
    "end_line": 364,
    "start_line": 363,
    "end_line": 368,
    "section": "Tabs"
  },
  {
    "markdown": "    a\ta\n    ὐ\ta\n",
    "html": "<pre><code>a\ta\nὐ\ta\n</code></pre>\n",
    "example": 3,
    "start_line": 366,
    "end_line": 373,
    "start_line": 370,
    "end_line": 377,
    "section": "Tabs"
  },
  {
    "markdown": "  - foo\n\n\tbar\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n",
    "example": 4,
    "start_line": 379,
    "end_line": 390,
    "start_line": 383,
    "end_line": 394,
    "section": "Tabs"
  },
  {
    "markdown": "- foo\n\n\t\tbar\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<pre><code>  bar\n</code></pre>\n</li>\n</ul>\n",
    "example": 5,
    "start_line": 392,
    "end_line": 404,
    "start_line": 396,
    "end_line": 408,
    "section": "Tabs"
  },
  {
    "markdown": ">\t\tfoo\n",
    "html": "<blockquote>\n<pre><code>  foo\n</code></pre>\n</blockquote>\n",
    "example": 6,
    "start_line": 415,
    "end_line": 422,
    "start_line": 419,
    "end_line": 426,
    "section": "Tabs"
  },
  {
    "markdown": "-\t\tfoo\n",
    "html": "<ul>\n<li>\n<pre><code>  foo\n</code></pre>\n</li>\n</ul>\n",
    "example": 7,
    "start_line": 424,
    "end_line": 433,
    "start_line": 428,
    "end_line": 437,
    "section": "Tabs"
  },
  {
    "markdown": "    foo\n\tbar\n",
    "html": "<pre><code>foo\nbar\n</code></pre>\n",
    "example": 8,
    "start_line": 436,
    "end_line": 443,
    "start_line": 440,
    "end_line": 447,
    "section": "Tabs"
  },
  {
    "markdown": " - foo\n   - bar\n\t - baz\n",
    "html": "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>baz</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n",
    "example": 9,
    "start_line": 445,
    "end_line": 461,
    "start_line": 449,
    "end_line": 465,
    "section": "Tabs"
  },
  {
    "markdown": "#\tFoo\n",
    "html": "<h1>Foo</h1>\n",
    "example": 10,
    "start_line": 463,
    "end_line": 467,
    "start_line": 467,
    "end_line": 471,
    "section": "Tabs"
  },
  {
    "markdown": "*\t*\t*\t\n",
    "html": "<hr />\n",
    "example": 11,
    "start_line": 469,
    "end_line": 473,
    "start_line": 473,
    "end_line": 477,
    "section": "Tabs"
  },
  {
    "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n",
    "html": "<p>!&quot;#$%&amp;'()*+,-./:;&lt;=&gt;?@[\\]^_`{|}~</p>\n",
    "example": 12,
    "start_line": 490,
    "end_line": 494,
    "section": "Backslash escapes"
  },
  {
    "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n",
    "html": "<p>\\\t\\A\\a\\ \\3\\φ\\«</p>\n",
    "example": 13,
    "start_line": 500,
    "end_line": 504,
    "section": "Backslash escapes"
  },
  {
    "markdown": "\\*not emphasized*\n\\<br/> not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\&ouml; not a character entity\n",
    "html": "<p>*not emphasized*\n&lt;br/&gt; not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url &quot;not a reference&quot;\n&amp;ouml; not a character entity</p>\n",
    "example": 14,
    "start_line": 510,
    "end_line": 530,
    "section": "Backslash escapes"
  },
  {
    "markdown": "\\\\*emphasis*\n",
    "html": "<p>\\<em>emphasis</em></p>\n",
    "example": 15,
    "start_line": 535,
    "end_line": 539,
    "section": "Backslash escapes"
  },
  {
    "markdown": "foo\\\nbar\n",
    "html": "<p>foo<br />\nbar</p>\n",
    "example": 16,
    "start_line": 544,
    "end_line": 550,
    "section": "Backslash escapes"
  },
  {
    "markdown": "`` \\[\\` ``\n",
    "html": "<p><code>\\[\\`</code></p>\n",
    "example": 17,
    "start_line": 556,
    "end_line": 560,
    "section": "Backslash escapes"
  },
  {
    "markdown": "    \\[\\]\n",
    "html": "<pre><code>\\[\\]\n</code></pre>\n",
    "example": 18,
    "start_line": 563,
    "end_line": 568,
    "section": "Backslash escapes"
  },
  {
    "markdown": "~~~\n\\[\\]\n~~~\n",
    "html": "<pre><code>\\[\\]\n</code></pre>\n",
    "example": 19,
    "start_line": 571,
    "end_line": 578,
    "section": "Backslash escapes"
  },
  {
    "markdown": "<http://example.com?find=\\*>\n",
    "html": "<p><a href=\"http://example.com?find=%5C*\">http://example.com?find=\\*</a></p>\n",
    "example": 20,
    "start_line": 581,
    "end_line": 585,
    "section": "Backslash escapes"
  },
  {
    "markdown": "<a href=\"/bar\\/)\">\n",
    "html": "<a href=\"/bar\\/)\">\n",
    "example": 21,
    "start_line": 588,
    "end_line": 592,
    "section": "Backslash escapes"
  },
  {
    "markdown": "[foo](/bar\\* \"ti\\*tle\")\n",
    "html": "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n",
    "example": 22,
    "start_line": 598,
    "end_line": 602,
    "section": "Backslash escapes"
  },
  {
    "markdown": "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n",
    "html": "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n",
    "example": 23,
    "start_line": 605,
    "end_line": 611,
    "section": "Backslash escapes"
  },
  {
    "markdown": "``` foo\\+bar\nfoo\n```\n",
    "html": "<pre><code class=\"language-foo+bar\">foo\n</code></pre>\n",
    "example": 24,
    "start_line": 614,
    "end_line": 621,
    "section": "Backslash escapes"
  },
  {
    "markdown": "&nbsp; &amp; &copy; &AElig; &Dcaron;\n&frac34; &HilbertSpace; &DifferentialD;\n&ClockwiseContourIntegral; &ngE;\n",
    "html": "<p>  &amp; © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸</p>\n",
    "example": 25,
    "start_line": 650,
    "end_line": 658,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#35; &#1234; &#992; &#0;\n",
    "html": "<p># Ӓ Ϡ �</p>\n",
    "example": 26,
    "start_line": 669,
    "end_line": 673,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#X22; &#XD06; &#xcab;\n",
    "html": "<p>&quot; ആ ಫ</p>\n",
    "example": 27,
    "start_line": 682,
    "end_line": 686,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&nbsp &x; &#; &#x;\n&#87654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n",
    "html": "<p>&amp;nbsp &amp;x; &amp;#; &amp;#x;\n&amp;#87654321;\n&amp;#abcdef0;\n&amp;ThisIsNotDefined; &amp;hi?;</p>\n",
    "example": 28,
    "start_line": 691,
    "end_line": 701,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&copy\n",
    "html": "<p>&amp;copy</p>\n",
    "example": 29,
    "start_line": 708,
    "end_line": 712,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&MadeUpEntity;\n",
    "html": "<p>&amp;MadeUpEntity;</p>\n",
    "example": 30,
    "start_line": 718,
    "end_line": 722,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "<a href=\"&ouml;&ouml;.html\">\n",
    "html": "<a href=\"&ouml;&ouml;.html\">\n",
    "example": 31,
    "start_line": 729,
    "end_line": 733,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "[foo](/f&ouml;&ouml; \"f&ouml;&ouml;\")\n",
    "html": "<p><a href=\"/f%C3%B6%C3%B6\" title=\"föö\">foo</a></p>\n",
    "example": 32,
    "start_line": 736,
    "end_line": 740,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "[foo]\n\n[foo]: /f&ouml;&ouml; \"f&ouml;&ouml;\"\n",
    "html": "<p><a href=\"/f%C3%B6%C3%B6\" title=\"föö\">foo</a></p>\n",
    "example": 33,
    "start_line": 743,
    "end_line": 749,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "``` f&ouml;&ouml;\nfoo\n```\n",
    "html": "<pre><code class=\"language-föö\">foo\n</code></pre>\n",
    "example": 34,
    "start_line": 752,
    "end_line": 759,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "`f&ouml;&ouml;`\n",
    "html": "<p><code>f&amp;ouml;&amp;ouml;</code></p>\n",
    "example": 35,
    "start_line": 765,
    "end_line": 769,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "    f&ouml;f&ouml;\n",
    "html": "<pre><code>f&amp;ouml;f&amp;ouml;\n</code></pre>\n",
    "example": 36,
    "start_line": 772,
    "end_line": 777,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#42;foo&#42;\n*foo*\n",
    "html": "<p>*foo*\n<em>foo</em></p>\n",
    "example": 37,
    "start_line": 784,
    "end_line": 790,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#42; foo\n\n* foo\n",
    "html": "<p>* foo</p>\n<ul>\n<li>foo</li>\n</ul>\n",
    "example": 38,
    "start_line": 792,
    "end_line": 801,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "foo&#10;&#10;bar\n",
    "html": "<p>foo\n\nbar</p>\n",
    "example": 39,
    "start_line": 803,
    "end_line": 809,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#9;foo\n",
    "html": "<p>\tfoo</p>\n",
    "example": 40,
    "start_line": 811,
    "end_line": 815,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "[a](url &quot;tit&quot;)\n",
    "html": "<p>[a](url &quot;tit&quot;)</p>\n",
    "example": 41,
    "start_line": 818,
    "end_line": 822,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "- `one\n- two`\n",
    "html": "<ul>\n<li>`one</li>\n<li>two`</li>\n</ul>\n",
    "example": 12,
    "start_line": 496,
    "end_line": 504,
    "example": 42,
    "start_line": 841,
    "end_line": 849,
    "section": "Precedence"
  },
  {
    "markdown": "***\n---\n___\n",
    "html": "<hr />\n<hr />\n<hr />\n",
    "example": 13,
    "start_line": 535,
    "end_line": 543,
    "example": 43,
    "start_line": 880,
    "end_line": 888,
    "section": "Thematic breaks"
  },
  {
    "markdown": "+++\n",
    "html": "<p>+++</p>\n",
    "example": 14,
    "start_line": 548,
    "end_line": 552,
    "example": 44,
    "start_line": 893,
    "end_line": 897,
    "section": "Thematic breaks"
  },
  {
    "markdown": "===\n",
    "html": "<p>===</p>\n",
    "example": 15,
    "start_line": 555,
    "end_line": 559,
    "example": 45,
    "start_line": 900,
    "end_line": 904,
    "section": "Thematic breaks"
  },
  {
    "markdown": "--\n**\n__\n",
    "html": "<p>--\n**\n__</p>\n",
    "example": 16,
    "start_line": 564,
    "end_line": 572,
    "example": 46,
    "start_line": 909,
    "end_line": 917,
    "section": "Thematic breaks"
  },
  {
    "markdown": " ***\n  ***\n   ***\n",
    "html": "<hr />\n<hr />\n<hr />\n",
    "example": 17,
    "start_line": 577,
    "end_line": 585,
    "example": 47,
    "start_line": 922,
    "end_line": 930,
    "section": "Thematic breaks"
  },
  {
    "markdown": "    ***\n",
    "html": "<pre><code>***\n</code></pre>\n",
    "example": 18,
    "start_line": 590,
    "end_line": 595,
    "example": 48,
    "start_line": 935,
    "end_line": 940,
    "section": "Thematic breaks"
  },
  {
    "markdown": "Foo\n    ***\n",
    "html": "<p>Foo\n***</p>\n",
    "example": 19,
    "start_line": 598,
    "end_line": 604,
    "example": 49,
    "start_line": 943,
    "end_line": 949,
    "section": "Thematic breaks"
  },
  {
    "markdown": "_____________________________________\n",
    "html": "<hr />\n",
    "example": 20,
    "start_line": 609,
    "end_line": 613,
    "example": 50,
    "start_line": 954,
    "end_line": 958,
    "section": "Thematic breaks"
  },
  {
    "markdown": " - - -\n",
    "html": "<hr />\n",
    "example": 21,
    "start_line": 618,
    "end_line": 622,
    "example": 51,
    "start_line": 963,
    "end_line": 967,
    "section": "Thematic breaks"
  },
  {
    "markdown": " **  * ** * ** * **\n",
    "html": "<hr />\n",
    "example": 22,
    "start_line": 625,
    "end_line": 629,
    "example": 52,
    "start_line": 970,
    "end_line": 974,
    "section": "Thematic breaks"
  },
  {
    "markdown": "-     -      -      -\n",
    "html": "<hr />\n",
    "example": 23,
    "start_line": 632,
    "end_line": 636,
    "example": 53,
    "start_line": 977,
    "end_line": 981,
    "section": "Thematic breaks"
  },
  {
    "markdown": "- - - -    \n",
    "html": "<hr />\n",
    "example": 24,
    "start_line": 641,
    "end_line": 645,
    "example": 54,
    "start_line": 986,
    "end_line": 990,
    "section": "Thematic breaks"
  },
  {
    "markdown": "_ _ _ _ a\n\na------\n\n---a---\n",
    "html": "<p>_ _ _ _ a</p>\n<p>a------</p>\n<p>---a---</p>\n",
    "example": 25,
    "start_line": 650,
    "end_line": 660,
    "example": 55,
    "start_line": 995,
    "end_line": 1005,
    "section": "Thematic breaks"
  },
  {
    "markdown": " *-*\n",
    "html": "<p><em>-</em></p>\n",
    "example": 26,
    "start_line": 666,
    "end_line": 670,
    "example": 56,
    "start_line": 1011,
    "end_line": 1015,
    "section": "Thematic breaks"
  },
  {
    "markdown": "- foo\n***\n- bar\n",
    "html": "<ul>\n<li>foo</li>\n</ul>\n<hr />\n<ul>\n<li>bar</li>\n</ul>\n",
    "example": 27,
    "start_line": 675,
    "end_line": 687,
    "example": 57,
    "start_line": 1020,
    "end_line": 1032,
    "section": "Thematic breaks"
  },
  {
    "markdown": "Foo\n***\nbar\n",
    "html": "<p>Foo</p>\n<hr />\n<p>bar</p>\n",
    "example": 28,
    "start_line": 692,
    "end_line": 700,
    "example": 58,
    "start_line": 1037,
    "end_line": 1045,
    "section": "Thematic breaks"
  },
  {
    "markdown": "Foo\n---\nbar\n",
    "html": "<h2>Foo</h2>\n<p>bar</p>\n",
    "example": 29,
    "start_line": 709,
    "end_line": 716,
    "example": 59,
    "start_line": 1054,
    "end_line": 1061,
    "section": "Thematic breaks"
  },
  {
    "markdown": "* Foo\n* * *\n* Bar\n",
    "html": "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n<ul>\n<li>Bar</li>\n</ul>\n",
    "example": 30,
    "start_line": 722,
    "end_line": 734,
    "example": 60,
    "start_line": 1067,
    "end_line": 1079,
    "section": "Thematic breaks"
  },
  {
    "markdown": "- Foo\n- * * *\n",
    "html": "<ul>\n<li>Foo</li>\n<li>\n<hr />\n</li>\n</ul>\n",
    "example": 31,
    "start_line": 739,
    "end_line": 749,
    "example": 61,
    "start_line": 1084,
    "end_line": 1094,
    "section": "Thematic breaks"
  },
  {
    "markdown": "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n",
    "html": "<h1>foo</h1>\n<h2>foo</h2>\n<h3>foo</h3>\n<h4>foo</h4>\n<h5>foo</h5>\n<h6>foo</h6>\n",
    "example": 32,
    "start_line": 768,
    "end_line": 782,
    "example": 62,
    "start_line": 1113,
    "end_line": 1127,
    "section": "ATX headings"
  },
  {
    "markdown": "####### foo\n",
    "html": "<p>####### foo</p>\n",
    "example": 33,
    "start_line": 787,
    "end_line": 791,
    "example": 63,
    "start_line": 1132,
    "end_line": 1136,
    "section": "ATX headings"
  },
  {
    "markdown": "#5 bolt\n\n#hashtag\n",
    "html": "<p>#5 bolt</p>\n<p>#hashtag</p>\n",
    "example": 34,
    "start_line": 802,
    "end_line": 809,
    "example": 64,
    "start_line": 1147,
    "end_line": 1154,
    "section": "ATX headings"
  },
  {
    "markdown": "\\## foo\n",
    "html": "<p>## foo</p>\n",
    "example": 35,
    "start_line": 814,
    "end_line": 818,
    "example": 65,
    "start_line": 1159,
    "end_line": 1163,
    "section": "ATX headings"
  },
  {
    "markdown": "# foo *bar* \\*baz\\*\n",
    "html": "<h1>foo <em>bar</em> *baz*</h1>\n",
    "example": 36,
    "start_line": 823,
    "end_line": 827,
    "example": 66,
    "start_line": 1168,
    "end_line": 1172,
    "section": "ATX headings"
  },
  {
    "markdown": "#                  foo                     \n",
    "html": "<h1>foo</h1>\n",
    "example": 37,
    "start_line": 832,
    "end_line": 836,
    "example": 67,
    "start_line": 1177,
    "end_line": 1181,
    "section": "ATX headings"
  },
  {
    "markdown": " ### foo\n  ## foo\n   # foo\n",
    "html": "<h3>foo</h3>\n<h2>foo</h2>\n<h1>foo</h1>\n",
    "example": 38,
    "start_line": 841,
    "end_line": 849,
    "example": 68,
    "start_line": 1186,
    "end_line": 1194,
    "section": "ATX headings"
  },
  {
    "markdown": "    # foo\n",
    "html": "<pre><code># foo\n</code></pre>\n",
    "example": 39,
    "start_line": 854,
    "end_line": 859,
    "example": 69,
    "start_line": 1199,
    "end_line": 1204,
    "section": "ATX headings"
  },
  {
    "markdown": "foo\n    # bar\n",
    "html": "<p>foo\n# bar</p>\n",
    "example": 40,
    "start_line": 862,
    "end_line": 868,
    "example": 70,
    "start_line": 1207,
    "end_line": 1213,
    "section": "ATX headings"
  },
  {
    "markdown": "## foo ##\n  ###   bar    ###\n",
    "html": "<h2>foo</h2>\n<h3>bar</h3>\n",
    "example": 41,
    "start_line": 873,
    "end_line": 879,
    "example": 71,
    "start_line": 1218,
    "end_line": 1224,
    "section": "ATX headings"
  },
  {
    "markdown": "# foo ##################################\n##### foo ##\n",
    "html": "<h1>foo</h1>\n<h5>foo</h5>\n",
    "example": 42,
    "start_line": 884,
    "end_line": 890,
    "example": 72,
    "start_line": 1229,
    "end_line": 1235,
    "section": "ATX headings"
  },
  {
    "markdown": "### foo ###     \n",
    "html": "<h3>foo</h3>\n",
    "example": 43,
    "start_line": 895,
    "end_line": 899,
    "example": 73,
    "start_line": 1240,
    "end_line": 1244,
    "section": "ATX headings"
  },
  {
    "markdown": "### foo ### b\n",
    "html": "<h3>foo ### b</h3>\n",
    "example": 44,
    "start_line": 906,
    "end_line": 910,
    "example": 74,
    "start_line": 1251,
    "end_line": 1255,
    "section": "ATX headings"
  },
  {
    "markdown": "# foo#\n",
    "html": "<h1>foo#</h1>\n",
    "example": 45,
    "start_line": 915,
    "end_line": 919,
    "example": 75,
    "start_line": 1260,
    "end_line": 1264,
    "section": "ATX headings"
  },
  {
    "markdown": "### foo \\###\n## foo #\\##\n# foo \\#\n",
    "html": "<h3>foo ###</h3>\n<h2>foo ###</h2>\n<h1>foo #</h1>\n",
    "example": 46,
    "start_line": 925,
    "end_line": 933,
    "example": 76,
    "start_line": 1270,
    "end_line": 1278,
    "section": "ATX headings"
  },
  {
    "markdown": "****\n## foo\n****\n",
    "html": "<hr />\n<h2>foo</h2>\n<hr />\n",
    "example": 47,
    "start_line": 939,
    "end_line": 947,
    "example": 77,
    "start_line": 1284,
    "end_line": 1292,
    "section": "ATX headings"
  },
  {
    "markdown": "Foo bar\n# baz\nBar foo\n",
    "html": "<p>Foo bar</p>\n<h1>baz</h1>\n<p>Bar foo</p>\n",
    "example": 48,
    "start_line": 950,
    "end_line": 958,
    "example": 78,
    "start_line": 1295,
    "end_line": 1303,
    "section": "ATX headings"
  },
  {
    "markdown": "## \n#\n### ###\n",
    "html": "<h2></h2>\n<h1></h1>\n<h3></h3>\n",
    "example": 49,
    "start_line": 963,
    "end_line": 971,
    "example": 79,
    "start_line": 1308,
    "end_line": 1316,
    "section": "ATX headings"
  },
  {
    "markdown": "Foo *bar*\n=========\n\nFoo *bar*\n---------\n",
    "html": "<h1>Foo <em>bar</em></h1>\n<h2>Foo <em>bar</em></h2>\n",
    "example": 50,
    "start_line": 1006,
    "end_line": 1015,
    "example": 80,
    "start_line": 1351,
    "end_line": 1360,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo *bar\nbaz*\n====\n",
    "html": "<h1>Foo <em>bar\nbaz</em></h1>\n",
    "example": 51,
    "start_line": 1020,
    "end_line": 1027,
    "example": 81,
    "start_line": 1365,
    "end_line": 1372,
    "section": "Setext headings"
  },
  {
    "markdown": "  Foo *bar\nbaz*\t\n====\n",
    "html": "<h1>Foo <em>bar\nbaz</em></h1>\n",
    "example": 52,
    "start_line": 1034,
    "end_line": 1041,
    "example": 82,
    "start_line": 1379,
    "end_line": 1386,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n-------------------------\n\nFoo\n=\n",
    "html": "<h2>Foo</h2>\n<h1>Foo</h1>\n",
    "example": 53,
    "start_line": 1046,
    "end_line": 1055,
    "example": 83,
    "start_line": 1391,
    "end_line": 1400,
    "section": "Setext headings"
  },
  {
    "markdown": "   Foo\n---\n\n  Foo\n-----\n\n  Foo\n  ===\n",
    "html": "<h2>Foo</h2>\n<h2>Foo</h2>\n<h1>Foo</h1>\n",
    "example": 54,
    "start_line": 1061,
    "end_line": 1074,
    "example": 84,
    "start_line": 1406,
    "end_line": 1419,
    "section": "Setext headings"
  },
  {
    "markdown": "    Foo\n    ---\n\n    Foo\n---\n",
    "html": "<pre><code>Foo\n---\n\nFoo\n</code></pre>\n<hr />\n",
    "example": 55,
    "start_line": 1079,
    "end_line": 1092,
    "example": 85,
    "start_line": 1424,
    "end_line": 1437,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n   ----      \n",
    "html": "<h2>Foo</h2>\n",
    "example": 56,
    "start_line": 1098,
    "end_line": 1103,
    "example": 86,
    "start_line": 1443,
    "end_line": 1448,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n    ---\n",
    "html": "<p>Foo\n---</p>\n",
    "example": 57,
    "start_line": 1108,
    "end_line": 1114,
    "example": 87,
    "start_line": 1453,
    "end_line": 1459,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n= =\n\nFoo\n--- -\n",
    "html": "<p>Foo\n= =</p>\n<p>Foo</p>\n<hr />\n",
    "example": 58,
    "start_line": 1119,
    "end_line": 1130,
    "example": 88,
    "start_line": 1464,
    "end_line": 1475,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo  \n-----\n",
    "html": "<h2>Foo</h2>\n",
    "example": 59,
    "start_line": 1135,
    "end_line": 1140,
    "example": 89,
    "start_line": 1480,
    "end_line": 1485,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\\\n----\n",
    "html": "<h2>Foo\\</h2>\n",
    "example": 60,
    "start_line": 1145,
    "end_line": 1150,
    "example": 90,
    "start_line": 1490,
    "end_line": 1495,
    "section": "Setext headings"
  },
  {
    "markdown": "`Foo\n----\n`\n\n<a title=\"a lot\n---\nof dashes\"/>\n",
    "html": "<h2>`Foo</h2>\n<p>`</p>\n<h2>&lt;a title=&quot;a lot</h2>\n<p>of dashes&quot;/&gt;</p>\n",
    "example": 61,
    "start_line": 1156,
    "end_line": 1169,
    "example": 91,
    "start_line": 1501,
    "end_line": 1514,
    "section": "Setext headings"
  },
  {
    "markdown": "> Foo\n---\n",
    "html": "<blockquote>\n<p>Foo</p>\n</blockquote>\n<hr />\n",
    "example": 62,
    "start_line": 1175,
    "end_line": 1183,
    "example": 92,
    "start_line": 1520,
    "end_line": 1528,
    "section": "Setext headings"
  },
  {
    "markdown": "> foo\nbar\n===\n",
    "html": "<blockquote>\n<p>foo\nbar\n===</p>\n</blockquote>\n",
    "example": 63,
    "start_line": 1186,
    "end_line": 1196,
    "example": 93,
    "start_line": 1531,
    "end_line": 1541,
    "section": "Setext headings"
  },
  {
    "markdown": "- Foo\n---\n",
    "html": "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n",
    "example": 64,
    "start_line": 1199,
    "end_line": 1207,
    "example": 94,
    "start_line": 1544,
    "end_line": 1552,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nBar\n---\n",
    "html": "<h2>Foo\nBar</h2>\n",
    "example": 65,
    "start_line": 1214,
    "end_line": 1221,
    "example": 95,
    "start_line": 1559,
    "end_line": 1566,
    "section": "Setext headings"
  },
  {
    "markdown": "---\nFoo\n---\nBar\n---\nBaz\n",
    "html": "<hr />\n<h2>Foo</h2>\n<h2>Bar</h2>\n<p>Baz</p>\n",
    "example": 66,
    "start_line": 1227,
    "end_line": 1239,
    "example": 96,
    "start_line": 1572,
    "end_line": 1584,
    "section": "Setext headings"
  },
  {
    "markdown": "\n====\n",
    "html": "<p>====</p>\n",
    "example": 67,
    "start_line": 1244,
    "end_line": 1249,
    "example": 97,
    "start_line": 1589,
    "end_line": 1594,
    "section": "Setext headings"
  },
  {
    "markdown": "---\n---\n",
    "html": "<hr />\n<hr />\n",
    "example": 68,
    "start_line": 1256,
    "end_line": 1262,
    "example": 98,
    "start_line": 1601,
    "end_line": 1607,
    "section": "Setext headings"
  },
  {
    "markdown": "- foo\n-----\n",
    "html": "<ul>\n<li>foo</li>\n</ul>\n<hr />\n",
    "example": 69,
    "start_line": 1265,
    "end_line": 1273,
    "example": 99,
    "start_line": 1610,
    "end_line": 1618,
    "section": "Setext headings"
  },
  {
    "markdown": "    foo\n---\n",
    "html": "<pre><code>foo\n</code></pre>\n<hr />\n",
    "example": 70,
    "start_line": 1276,
    "end_line": 1283,
    "example": 100,
    "start_line": 1621,
    "end_line": 1628,
    "section": "Setext headings"
  },
  {
    "markdown": "> foo\n-----\n",
    "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n",
    "example": 71,
    "start_line": 1286,
    "end_line": 1294,
    "example": 101,
    "start_line": 1631,
    "end_line": 1639,
    "section": "Setext headings"
  },
  {
    "markdown": "\\> foo\n------\n",
    "html": "<h2>&gt; foo</h2>\n",
    "example": 72,
    "start_line": 1300,
    "end_line": 1305,
    "example": 102,
    "start_line": 1645,
    "end_line": 1650,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n\nbar\n---\nbaz\n",
    "html": "<p>Foo</p>\n<h2>bar</h2>\n<p>baz</p>\n",
    "example": 73,
    "start_line": 1331,
    "end_line": 1341,
    "example": 103,
    "start_line": 1676,
    "end_line": 1686,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nbar\n\n---\n\nbaz\n",
    "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n",
    "example": 74,
    "start_line": 1347,
    "end_line": 1359,
    "example": 104,
    "start_line": 1692,
    "end_line": 1704,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nbar\n* * *\nbaz\n",
    "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n",
    "example": 75,
    "start_line": 1365,
    "end_line": 1375,
    "example": 105,
    "start_line": 1710,
    "end_line": 1720,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nbar\n\\---\nbaz\n",
    "html": "<p>Foo\nbar\n---\nbaz</p>\n",
    "example": 76,
    "start_line": 1380,
    "end_line": 1390,
    "example": 106,
    "start_line": 1725,
    "end_line": 1735,
    "section": "Setext headings"
  },
  {
    "markdown": "    a simple\n      indented code block\n",
    "html": "<pre><code>a simple\n  indented code block\n</code></pre>\n",
    "example": 77,
    "start_line": 1408,
    "end_line": 1415,
    "example": 107,
    "start_line": 1753,
    "end_line": 1760,
    "section": "Indented code blocks"
  },
  {
    "markdown": "  - foo\n\n    bar\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n",
    "example": 78,
    "start_line": 1422,
    "end_line": 1433,
    "example": 108,
    "start_line": 1767,
    "end_line": 1778,
    "section": "Indented code blocks"
  },
  {
    "markdown": "1.  foo\n\n    - bar\n",
    "html": "<ol>\n<li>\n<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n</li>\n</ol>\n",
    "example": 79,
    "start_line": 1436,
    "end_line": 1449,
    "example": 109,
    "start_line": 1781,
    "end_line": 1794,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    <a/>\n    *hi*\n\n    - one\n",
    "html": "<pre><code>&lt;a/&gt;\n*hi*\n\n- one\n</code></pre>\n",
    "example": 80,
    "start_line": 1456,
    "end_line": 1467,
    "example": 110,
    "start_line": 1801,
    "end_line": 1812,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    chunk1\n\n    chunk2\n  \n \n \n    chunk3\n",
    "html": "<pre><code>chunk1\n\nchunk2\n\n\n\nchunk3\n</code></pre>\n",
    "example": 81,
    "start_line": 1472,
    "end_line": 1489,
    "example": 111,
    "start_line": 1817,
    "end_line": 1834,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    chunk1\n      \n      chunk2\n",
    "html": "<pre><code>chunk1\n  \n  chunk2\n</code></pre>\n",
    "example": 82,
    "start_line": 1495,
    "end_line": 1504,
    "example": 112,
    "start_line": 1840,
    "end_line": 1849,
    "section": "Indented code blocks"
  },
  {
    "markdown": "Foo\n    bar\n\n",
    "html": "<p>Foo\nbar</p>\n",
    "example": 83,
    "start_line": 1510,
    "end_line": 1517,
    "example": 113,
    "start_line": 1855,
    "end_line": 1862,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    foo\nbar\n",
    "html": "<pre><code>foo\n</code></pre>\n<p>bar</p>\n",
    "example": 84,
    "start_line": 1524,
    "end_line": 1531,
    "example": 114,
    "start_line": 1869,
    "end_line": 1876,
    "section": "Indented code blocks"
  },
  {
    "markdown": "# Heading\n    foo\nHeading\n------\n    foo\n----\n",
    "html": "<h1>Heading</h1>\n<pre><code>foo\n</code></pre>\n<h2>Heading</h2>\n<pre><code>foo\n</code></pre>\n<hr />\n",
    "example": 85,
    "start_line": 1537,
    "end_line": 1552,
    "example": 115,
    "start_line": 1882,
    "end_line": 1897,
    "section": "Indented code blocks"
  },
  {
    "markdown": "        foo\n    bar\n",
    "html": "<pre><code>    foo\nbar\n</code></pre>\n",
    "example": 86,
    "start_line": 1557,
    "end_line": 1564,
    "example": 116,
    "start_line": 1902,
    "end_line": 1909,
    "section": "Indented code blocks"
  },
  {
    "markdown": "\n    \n    foo\n    \n\n",
    "html": "<pre><code>foo\n</code></pre>\n",
    "example": 87,
    "start_line": 1570,
    "end_line": 1579,
    "example": 117,
    "start_line": 1915,
    "end_line": 1924,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    foo  \n",
    "html": "<pre><code>foo  \n</code></pre>\n",
    "example": 88,
    "start_line": 1584,
    "end_line": 1589,
    "example": 118,
    "start_line": 1929,
    "end_line": 1934,
    "section": "Indented code blocks"
  },
  {
    "markdown": "```\n<\n >\n```\n",
    "html": "<pre><code>&lt;\n &gt;\n</code></pre>\n",
    "example": 89,
    "start_line": 1639,
    "end_line": 1648,
    "example": 119,
    "start_line": 1984,
    "end_line": 1993,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~\n<\n >\n~~~\n",
    "html": "<pre><code>&lt;\n &gt;\n</code></pre>\n",
    "example": 90,
    "start_line": 1653,
    "end_line": 1662,
    "example": 120,
    "start_line": 1998,
    "end_line": 2007,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "``\nfoo\n``\n",
    "html": "<p><code>foo</code></p>\n",
    "example": 91,
    "start_line": 1666,
    "end_line": 1672,
    "example": 121,
    "start_line": 2011,
    "end_line": 2017,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\naaa\n~~~\n```\n",
    "html": "<pre><code>aaa\n~~~\n</code></pre>\n",
    "example": 92,
    "start_line": 1677,
    "end_line": 1686,
    "example": 122,
    "start_line": 2022,
    "end_line": 2031,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~\naaa\n```\n~~~\n",
    "html": "<pre><code>aaa\n```\n</code></pre>\n",
    "example": 93,
    "start_line": 1689,
    "end_line": 1698,
    "example": 123,
    "start_line": 2034,
    "end_line": 2043,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "````\naaa\n```\n``````\n",
    "html": "<pre><code>aaa\n```\n</code></pre>\n",
    "example": 94,
    "start_line": 1703,
    "end_line": 1712,
    "example": 124,
    "start_line": 2048,
    "end_line": 2057,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~~\naaa\n~~~\n~~~~\n",
    "html": "<pre><code>aaa\n~~~\n</code></pre>\n",
    "example": 95,
    "start_line": 1715,
    "end_line": 1724,
    "example": 125,
    "start_line": 2060,
    "end_line": 2069,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n",
    "html": "<pre><code></code></pre>\n",
    "example": 96,
    "start_line": 1730,
    "end_line": 1734,
    "example": 126,
    "start_line": 2075,
    "end_line": 2079,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "`````\n\n```\naaa\n",
    "html": "<pre><code>\n```\naaa\n</code></pre>\n",
    "example": 97,
    "start_line": 1737,
    "end_line": 1747,
    "example": 127,
    "start_line": 2082,
    "end_line": 2092,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "> ```\n> aaa\n\nbbb\n",
    "html": "<blockquote>\n<pre><code>aaa\n</code></pre>\n</blockquote>\n<p>bbb</p>\n",
    "example": 98,
    "start_line": 1750,
    "end_line": 1761,
    "example": 128,
    "start_line": 2095,
    "end_line": 2106,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n\n  \n```\n",
    "html": "<pre><code>\n  \n</code></pre>\n",
    "example": 99,
    "start_line": 1766,
    "end_line": 1775,
    "example": 129,
    "start_line": 2111,
    "end_line": 2120,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n```\n",
    "html": "<pre><code></code></pre>\n",
    "example": 100,
    "start_line": 1780,
    "end_line": 1785,
    "example": 130,
    "start_line": 2125,
    "end_line": 2130,
    "section": "Fenced code blocks"
  },
  {
    "markdown": " ```\n aaa\naaa\n```\n",
    "html": "<pre><code>aaa\naaa\n</code></pre>\n",
    "example": 101,
    "start_line": 1792,
    "end_line": 1801,
    "example": 131,
    "start_line": 2137,
    "end_line": 2146,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "  ```\naaa\n  aaa\naaa\n  ```\n",
    "html": "<pre><code>aaa\naaa\naaa\n</code></pre>\n",
    "example": 102,
    "start_line": 1804,
    "end_line": 1815,
    "example": 132,
    "start_line": 2149,
    "end_line": 2160,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "   ```\n   aaa\n    aaa\n  aaa\n   ```\n",
    "html": "<pre><code>aaa\n aaa\naaa\n</code></pre>\n",
    "example": 103,
    "start_line": 1818,
    "end_line": 1829,
    "example": 133,
    "start_line": 2163,
    "end_line": 2174,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "    ```\n    aaa\n    ```\n",
    "html": "<pre><code>```\naaa\n```\n</code></pre>\n",
    "example": 104,
    "start_line": 1834,
    "end_line": 1843,
    "example": 134,
    "start_line": 2179,
    "end_line": 2188,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\naaa\n  ```\n",
    "html": "<pre><code>aaa\n</code></pre>\n",
    "example": 105,
    "start_line": 1849,
    "end_line": 1856,
    "example": 135,
    "start_line": 2194,
    "end_line": 2201,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "   ```\naaa\n  ```\n",
    "html": "<pre><code>aaa\n</code></pre>\n",
    "example": 106,
    "start_line": 1859,
    "end_line": 1866,
    "example": 136,
    "start_line": 2204,
    "end_line": 2211,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\naaa\n    ```\n",
    "html": "<pre><code>aaa\n    ```\n</code></pre>\n",
    "example": 107,
    "start_line": 1871,
    "end_line": 1879,
    "example": 137,
    "start_line": 2216,
    "end_line": 2224,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "``` ```\naaa\n",
    "html": "<p><code> </code>\naaa</p>\n",
    "example": 108,
    "start_line": 1885,
    "end_line": 1891,
    "example": 138,
    "start_line": 2230,
    "end_line": 2236,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~~~~\naaa\n~~~ ~~\n",
    "html": "<pre><code>aaa\n~~~ ~~\n</code></pre>\n",
    "example": 109,
    "start_line": 1894,
    "end_line": 1902,
    "example": 139,
    "start_line": 2239,
    "end_line": 2247,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "foo\n```\nbar\n```\nbaz\n",
    "html": "<p>foo</p>\n<pre><code>bar\n</code></pre>\n<p>baz</p>\n",
    "example": 110,
    "start_line": 1908,
    "end_line": 1919,
    "example": 140,
    "start_line": 2253,
    "end_line": 2264,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "foo\n---\n~~~\nbar\n~~~\n# baz\n",
    "html": "<h2>foo</h2>\n<pre><code>bar\n</code></pre>\n<h1>baz</h1>\n",
    "example": 111,
    "start_line": 1925,
    "end_line": 1937,
    "example": 141,
    "start_line": 2270,
    "end_line": 2282,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```ruby\ndef foo(x)\n  return 3\nend\n```\n",
    "html": "<pre><code class=\"language-ruby\">def foo(x)\n  return 3\nend\n</code></pre>\n",
    "example": 112,
    "start_line": 1947,
    "end_line": 1958,
    "example": 142,
    "start_line": 2292,
    "end_line": 2303,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~~    ruby startline=3 $%@#$\ndef foo(x)\n  return 3\nend\n~~~~~~~\n",
    "html": "<pre><code class=\"language-ruby\">def foo(x)\n  return 3\nend\n</code></pre>\n",
    "example": 113,
    "start_line": 1961,
    "end_line": 1972,
    "example": 143,
    "start_line": 2306,
    "end_line": 2317,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "````;\n````\n",
    "html": "<pre><code class=\"language-;\"></code></pre>\n",
    "example": 114,
    "start_line": 1975,
    "end_line": 1980,
    "example": 144,
    "start_line": 2320,
    "end_line": 2325,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "``` aa ```\nfoo\n",
    "html": "<p><code>aa</code>\nfoo</p>\n",
    "example": 115,
    "start_line": 1985,
    "end_line": 1991,
    "example": 145,
    "start_line": 2330,
    "end_line": 2336,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~ aa ``` ~~~\nfoo\n~~~\n",
    "html": "<pre><code class=\"language-aa\">foo\n</code></pre>\n",
    "example": 116,
    "start_line": 1996,
    "end_line": 2003,
    "example": 146,
    "start_line": 2341,
    "end_line": 2348,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n``` aaa\n```\n",
    "html": "<pre><code>``` aaa\n</code></pre>\n",
    "example": 117,
    "start_line": 2008,
    "end_line": 2015,
    "example": 147,
    "start_line": 2353,
    "end_line": 2360,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "<table><tr><td>\n<pre>\n**Hello**,\n\n_world_.\n</pre>\n</td></tr></table>\n",
    "html": "<table><tr><td>\n<pre>\n**Hello**,\n<p><em>world</em>.\n</pre></p>\n</td></tr></table>\n",
    "example": 118,
    "start_line": 2087,
    "end_line": 2102,
    "example": 148,
    "start_line": 2432,
    "end_line": 2447,
    "section": "HTML blocks"
  },
  {
    "markdown": "<table>\n  <tr>\n    <td>\n           hi\n    </td>\n  </tr>\n</table>\n\nokay.\n",
    "html": "<table>\n  <tr>\n    <td>\n           hi\n    </td>\n  </tr>\n</table>\n<p>okay.</p>\n",
    "example": 119,
    "start_line": 2116,
    "end_line": 2135,
    "example": 149,
    "start_line": 2461,
    "end_line": 2480,
    "section": "HTML blocks"
  },
  {
    "markdown": " <div>\n  *hello*\n         <foo><a>\n",
    "html": " <div>\n  *hello*\n         <foo><a>\n",
    "example": 120,
    "start_line": 2138,
    "end_line": 2146,
    "example": 150,
    "start_line": 2483,
    "end_line": 2491,
    "section": "HTML blocks"
  },
  {
    "markdown": "</div>\n*foo*\n",
    "html": "</div>\n*foo*\n",
    "example": 121,
    "start_line": 2151,
    "end_line": 2157,
    "example": 151,
    "start_line": 2496,
    "end_line": 2502,
    "section": "HTML blocks"
  },
  {
    "markdown": "<DIV CLASS=\"foo\">\n\n*Markdown*\n\n</DIV>\n",
    "html": "<DIV CLASS=\"foo\">\n<p><em>Markdown</em></p>\n</DIV>\n",
    "example": 122,
    "start_line": 2162,
    "end_line": 2172,
    "example": 152,
    "start_line": 2507,
    "end_line": 2517,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div id=\"foo\"\n  class=\"bar\">\n</div>\n",
    "html": "<div id=\"foo\"\n  class=\"bar\">\n</div>\n",
    "example": 123,
    "start_line": 2178,
    "end_line": 2186,
    "example": 153,
    "start_line": 2523,
    "end_line": 2531,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div id=\"foo\" class=\"bar\n  baz\">\n</div>\n",
    "html": "<div id=\"foo\" class=\"bar\n  baz\">\n</div>\n",
    "example": 124,
    "start_line": 2189,
    "end_line": 2197,
    "example": 154,
    "start_line": 2534,
    "end_line": 2542,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div>\n*foo*\n\n*bar*\n",
    "html": "<div>\n*foo*\n<p><em>bar</em></p>\n",
    "example": 125,
    "start_line": 2201,
    "end_line": 2210,
    "example": 155,
    "start_line": 2546,
    "end_line": 2555,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div id=\"foo\"\n*hi*\n",
    "html": "<div id=\"foo\"\n*hi*\n",
    "example": 126,
    "start_line": 2217,
    "end_line": 2223,
    "example": 156,
    "start_line": 2562,
    "end_line": 2568,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div class\nfoo\n",
    "html": "<div class\nfoo\n",
    "example": 127,
    "start_line": 2226,
    "end_line": 2232,
    "example": 157,
    "start_line": 2571,
    "end_line": 2577,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div *???-&&&-<---\n*foo*\n",
    "html": "<div *???-&&&-<---\n*foo*\n",
    "example": 128,
    "start_line": 2238,
    "end_line": 2244,
    "example": 158,
    "start_line": 2583,
    "end_line": 2589,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div><a href=\"bar\">*foo*</a></div>\n",
    "html": "<div><a href=\"bar\">*foo*</a></div>\n",
    "example": 129,
    "start_line": 2250,
    "end_line": 2254,
    "example": 159,
    "start_line": 2595,
    "end_line": 2599,
    "section": "HTML blocks"
  },
  {
    "markdown": "<table><tr><td>\nfoo\n</td></tr></table>\n",
    "html": "<table><tr><td>\nfoo\n</td></tr></table>\n",
    "example": 130,
    "start_line": 2257,
    "end_line": 2265,
    "example": 160,
    "start_line": 2602,
    "end_line": 2610,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div></div>\n``` c\nint x = 33;\n```\n",
    "html": "<div></div>\n``` c\nint x = 33;\n```\n",
    "example": 131,
    "start_line": 2274,
    "end_line": 2284,
    "example": 161,
    "start_line": 2619,
    "end_line": 2629,
    "section": "HTML blocks"
  },
  {
    "markdown": "<a href=\"foo\">\n*bar*\n</a>\n",
    "html": "<a href=\"foo\">\n*bar*\n</a>\n",
    "example": 132,
    "start_line": 2291,
    "end_line": 2299,
    "example": 162,
    "start_line": 2636,
    "end_line": 2644,
    "section": "HTML blocks"
  },
  {
    "markdown": "<Warning>\n*bar*\n</Warning>\n",
    "html": "<Warning>\n*bar*\n</Warning>\n",
    "example": 133,
    "start_line": 2304,
    "end_line": 2312,
    "example": 163,
    "start_line": 2649,
    "end_line": 2657,
    "section": "HTML blocks"
  },
  {
    "markdown": "<i class=\"foo\">\n*bar*\n</i>\n",
    "html": "<i class=\"foo\">\n*bar*\n</i>\n",
    "example": 134,
    "start_line": 2315,
    "end_line": 2323,
    "example": 164,
    "start_line": 2660,
    "end_line": 2668,
    "section": "HTML blocks"
  },
  {
    "markdown": "</ins>\n*bar*\n",
    "html": "</ins>\n*bar*\n",
    "example": 135,
    "start_line": 2326,
    "end_line": 2332,
    "example": 165,
    "start_line": 2671,
    "end_line": 2677,
    "section": "HTML blocks"
  },
  {
    "markdown": "<del>\n*foo*\n</del>\n",
    "html": "<del>\n*foo*\n</del>\n",
    "example": 136,
    "start_line": 2341,
    "end_line": 2349,
    "example": 166,
    "start_line": 2686,
    "end_line": 2694,
    "section": "HTML blocks"
  },
  {
    "markdown": "<del>\n\n*foo*\n\n</del>\n",
    "html": "<del>\n<p><em>foo</em></p>\n</del>\n",
    "example": 137,
    "start_line": 2356,
    "end_line": 2366,
    "example": 167,
    "start_line": 2701,
    "end_line": 2711,
    "section": "HTML blocks"
  },
  {
    "markdown": "<del>*foo*</del>\n",
    "html": "<p><del><em>foo</em></del></p>\n",
    "example": 138,
    "start_line": 2374,
    "end_line": 2378,
    "example": 168,
    "start_line": 2719,
    "end_line": 2723,
    "section": "HTML blocks"
  },
  {
    "markdown": "<pre language=\"haskell\"><code>\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n</code></pre>\nokay\n",
    "html": "<pre language=\"haskell\"><code>\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n</code></pre>\n<p>okay</p>\n",
    "example": 139,
    "start_line": 2390,
    "end_line": 2406,
    "example": 169,
    "start_line": 2735,
    "end_line": 2751,
    "section": "HTML blocks"
  },
  {
    "markdown": "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n",
    "html": "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\n<p>okay</p>\n",
    "example": 140,
    "start_line": 2411,
    "end_line": 2425,
    "example": 170,
    "start_line": 2756,
    "end_line": 2770,
    "section": "HTML blocks"
  },
  {
    "markdown": "<textarea>\n\n*foo*\n\n_bar_\n\n</textarea>\n",
    "html": "<textarea>\n\n*foo*\n\n_bar_\n\n</textarea>\n",
    "example": 171,
    "start_line": 2775,
    "end_line": 2791,
    "section": "HTML blocks"
  },
  {
    "markdown": "<style\n  type=\"text/css\">\nh1 {color:red;}\n\np {color:blue;}\n</style>\nokay\n",
    "html": "<style\n  type=\"text/css\">\nh1 {color:red;}\n\np {color:blue;}\n</style>\n<p>okay</p>\n",
    "example": 141,
    "start_line": 2430,
    "end_line": 2446,
    "example": 172,
    "start_line": 2795,
    "end_line": 2811,
    "section": "HTML blocks"
  },
  {
    "markdown": "<style\n  type=\"text/css\">\n\nfoo\n",
    "html": "<style\n  type=\"text/css\">\n\nfoo\n",
    "example": 142,
    "start_line": 2453,
    "end_line": 2463,
    "example": 173,
    "start_line": 2818,
    "end_line": 2828,
    "section": "HTML blocks"
  },
  {
    "markdown": "> <div>\n> foo\n\nbar\n",
    "html": "<blockquote>\n<div>\nfoo\n</blockquote>\n<p>bar</p>\n",
    "example": 143,
    "start_line": 2466,
    "end_line": 2477,
    "example": 174,
    "start_line": 2831,
    "end_line": 2842,
    "section": "HTML blocks"
  },
  {
    "markdown": "- <div>\n- foo\n",
    "html": "<ul>\n<li>\n<div>\n</li>\n<li>foo</li>\n</ul>\n",
    "example": 144,
    "start_line": 2480,
    "end_line": 2490,
    "example": 175,
    "start_line": 2845,
    "end_line": 2855,
    "section": "HTML blocks"
  },
  {
    "markdown": "<style>p{color:red;}</style>\n*foo*\n",
    "html": "<style>p{color:red;}</style>\n<p><em>foo</em></p>\n",
    "example": 145,
    "start_line": 2495,
    "end_line": 2501,
    "example": 176,
    "start_line": 2860,
    "end_line": 2866,
    "section": "HTML blocks"
  },
  {
    "markdown": "<!-- foo -->*bar*\n*baz*\n",
    "html": "<!-- foo -->*bar*\n<p><em>baz</em></p>\n",
    "example": 146,
    "start_line": 2504,
    "end_line": 2510,
    "example": 177,
    "start_line": 2869,
    "end_line": 2875,
    "section": "HTML blocks"
  },
  {
    "markdown": "<script>\nfoo\n</script>1. *bar*\n",
    "html": "<script>\nfoo\n</script>1. *bar*\n",
    "example": 147,
    "start_line": 2516,
    "end_line": 2524,
    "example": 178,
    "start_line": 2881,
    "end_line": 2889,
    "section": "HTML blocks"
  },
  {
    "markdown": "<!-- Foo\n\nbar\n   baz -->\nokay\n",
    "html": "<!-- Foo\n\nbar\n   baz -->\n<p>okay</p>\n",
    "example": 148,
    "start_line": 2529,
    "end_line": 2541,
    "example": 179,
    "start_line": 2894,
    "end_line": 2906,
    "section": "HTML blocks"
  },
  {
    "markdown": "<?php\n\n  echo '>';\n\n?>\nokay\n",
    "html": "<?php\n\n  echo '>';\n\n?>\n<p>okay</p>\n",
    "example": 149,
    "start_line": 2547,
    "end_line": 2561,
    "example": 180,
    "start_line": 2912,
    "end_line": 2926,
    "section": "HTML blocks"
  },
  {
    "markdown": "<!DOCTYPE html>\n",
    "html": "<!DOCTYPE html>\n",
    "example": 150,
    "start_line": 2566,
    "end_line": 2570,
    "example": 181,
    "start_line": 2931,
    "end_line": 2935,
    "section": "HTML blocks"
  },
  {
    "markdown": "<![CDATA[\nfunction matchwo(a,b)\n{\n  if (a < b && a < 0) then {\n    return 1;\n\n  } else {\n\n    return 0;\n  }\n}\n]]>\nokay\n",
    "html": "<![CDATA[\nfunction matchwo(a,b)\n{\n  if (a < b && a < 0) then {\n    return 1;\n\n  } else {\n\n    return 0;\n  }\n}\n]]>\n<p>okay</p>\n",
    "example": 151,
    "start_line": 2575,
    "end_line": 2603,
    "example": 182,
    "start_line": 2940,
    "end_line": 2968,
    "section": "HTML blocks"
  },
  {
    "markdown": "  <!-- foo -->\n\n    <!-- foo -->\n",
    "html": "  <!-- foo -->\n<pre><code>&lt;!-- foo --&gt;\n</code></pre>\n",
    "example": 152,
    "start_line": 2608,
    "end_line": 2616,
    "example": 183,
    "start_line": 2974,
    "end_line": 2982,
    "section": "HTML blocks"
  },
  {
    "markdown": "  <div>\n\n    <div>\n",
    "html": "  <div>\n<pre><code>&lt;div&gt;\n</code></pre>\n",
    "example": 153,
    "start_line": 2619,
    "end_line": 2627,
    "example": 184,
    "start_line": 2985,
    "end_line": 2993,
    "section": "HTML blocks"
  },
  {
    "markdown": "Foo\n<div>\nbar\n</div>\n",
    "html": "<p>Foo</p>\n<div>\nbar\n</div>\n",
    "example": 154,
    "start_line": 2633,
    "end_line": 2643,
    "example": 185,
    "start_line": 2999,
    "end_line": 3009,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div>\nbar\n</div>\n*foo*\n",
    "html": "<div>\nbar\n</div>\n*foo*\n",
    "example": 155,
    "start_line": 2650,
    "end_line": 2660,
    "example": 186,
    "start_line": 3016,
    "end_line": 3026,
    "section": "HTML blocks"
  },
  {
    "markdown": "Foo\n<a href=\"bar\">\nbaz\n",
    "html": "<p>Foo\n<a href=\"bar\">\nbaz</p>\n",
    "example": 156,
    "start_line": 2665,
    "end_line": 2673,
    "example": 187,
    "start_line": 3031,
    "end_line": 3039,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div>\n\n*Emphasized* text.\n\n</div>\n",
    "html": "<div>\n<p><em>Emphasized</em> text.</p>\n</div>\n",
    "example": 157,
    "start_line": 2706,
    "end_line": 2716,
    "example": 188,
    "start_line": 3072,
    "end_line": 3082,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div>\n*Emphasized* text.\n</div>\n",
    "html": "<div>\n*Emphasized* text.\n</div>\n",
    "example": 158,
    "start_line": 2719,
    "end_line": 2727,
    "example": 189,
    "start_line": 3085,
    "end_line": 3093,
    "section": "HTML blocks"
  },
  {
    "markdown": "<table>\n\n<tr>\n\n<td>\nHi\n</td>\n\n</tr>\n\n</table>\n",
    "html": "<table>\n<tr>\n<td>\nHi\n</td>\n</tr>\n</table>\n",
    "example": 159,
    "start_line": 2741,
    "end_line": 2761,
    "example": 190,
    "start_line": 3107,
    "end_line": 3127,
    "section": "HTML blocks"
  },
  {
    "markdown": "<table>\n\n  <tr>\n\n    <td>\n      Hi\n    </td>\n\n  </tr>\n\n</table>\n",
    "html": "<table>\n  <tr>\n<pre><code>&lt;td&gt;\n  Hi\n&lt;/td&gt;\n</code></pre>\n  </tr>\n</table>\n",
    "example": 160,
    "start_line": 2768,
    "end_line": 2789,
    "example": 191,
    "start_line": 3134,
    "end_line": 3155,
    "section": "HTML blocks"
  },
  {
    "markdown": "[foo]: /url \"title\"\n\n[foo]\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 161,
    "start_line": 2816,
    "end_line": 2822,
    "example": 192,
    "start_line": 3183,
    "end_line": 3189,
    "section": "Link reference definitions"
  },
  {
    "markdown": "   [foo]: \n      /url  \n           'the title'  \n\n[foo]\n",
    "html": "<p><a href=\"/url\" title=\"the title\">foo</a></p>\n",
    "example": 162,
    "start_line": 2825,
    "end_line": 2833,
    "example": 193,
    "start_line": 3192,
    "end_line": 3200,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n",
    "html": "<p><a href=\"my_(url)\" title=\"title (with parens)\">Foo*bar]</a></p>\n",
    "example": 163,
    "start_line": 2836,
    "end_line": 2842,
    "example": 194,
    "start_line": 3203,
    "end_line": 3209,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[Foo bar]:\n<my url>\n'title'\n\n[Foo bar]\n",
    "html": "<p><a href=\"my%20url\" title=\"title\">Foo bar</a></p>\n",
    "example": 164,
    "start_line": 2845,
    "end_line": 2853,
    "example": 195,
    "start_line": 3212,
    "end_line": 3220,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n",
    "html": "<p><a href=\"/url\" title=\"\ntitle\nline1\nline2\n\">foo</a></p>\n",
    "example": 165,
    "start_line": 2858,
    "end_line": 2872,
    "example": 196,
    "start_line": 3225,
    "end_line": 3239,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n",
    "html": "<p>[foo]: /url 'title</p>\n<p>with blank line'</p>\n<p>[foo]</p>\n",
    "example": 166,
    "start_line": 2877,
    "end_line": 2887,
    "example": 197,
    "start_line": 3244,
    "end_line": 3254,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]:\n/url\n\n[foo]\n",
    "html": "<p><a href=\"/url\">foo</a></p>\n",
    "example": 167,
    "start_line": 2892,
    "end_line": 2899,
    "example": 198,
    "start_line": 3259,
    "end_line": 3266,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]:\n\n[foo]\n",
    "html": "<p>[foo]:</p>\n<p>[foo]</p>\n",
    "example": 168,
    "start_line": 2904,
    "end_line": 2911,
    "example": 199,
    "start_line": 3271,
    "end_line": 3278,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: <>\n\n[foo]\n",
    "html": "<p><a href=\"\">foo</a></p>\n",
    "example": 169,
    "start_line": 2916,
    "end_line": 2922,
    "example": 200,
    "start_line": 3283,
    "end_line": 3289,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: <bar>(baz)\n\n[foo]\n",
    "html": "<p>[foo]: <bar>(baz)</p>\n<p>[foo]</p>\n",
    "example": 170,
    "start_line": 2927,
    "end_line": 2934,
    "example": 201,
    "start_line": 3294,
    "end_line": 3301,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n",
    "html": "<p><a href=\"/url%5Cbar*baz\" title=\"foo&quot;bar\\baz\">foo</a></p>\n",
    "example": 171,
    "start_line": 2940,
    "end_line": 2946,
    "example": 202,
    "start_line": 3307,
    "end_line": 3313,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]\n\n[foo]: url\n",
    "html": "<p><a href=\"url\">foo</a></p>\n",
    "example": 172,
    "start_line": 2951,
    "end_line": 2957,
    "example": 203,
    "start_line": 3318,
    "end_line": 3324,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n",
    "html": "<p><a href=\"first\">foo</a></p>\n",
    "example": 173,
    "start_line": 2963,
    "end_line": 2970,
    "example": 204,
    "start_line": 3330,
    "end_line": 3337,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[FOO]: /url\n\n[Foo]\n",
    "html": "<p><a href=\"/url\">Foo</a></p>\n",
    "example": 174,
    "start_line": 2976,
    "end_line": 2982,
    "example": 205,
    "start_line": 3343,
    "end_line": 3349,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n",
    "html": "<p><a href=\"/%CF%86%CE%BF%CF%85\">αγω</a></p>\n",
    "example": 175,
    "start_line": 2985,
    "end_line": 2991,
    "example": 206,
    "start_line": 3352,
    "end_line": 3358,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\n",
    "html": "",
    "example": 176,
    "start_line": 2997,
    "end_line": 3000,
    "example": 207,
    "start_line": 3367,
    "end_line": 3370,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[\nfoo\n]: /url\nbar\n",
    "html": "<p>bar</p>\n",
    "example": 177,
    "start_line": 3005,
    "end_line": 3012,
    "example": 208,
    "start_line": 3375,
    "end_line": 3382,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url \"title\" ok\n",
    "html": "<p>[foo]: /url &quot;title&quot; ok</p>\n",
    "example": 178,
    "start_line": 3018,
    "end_line": 3022,
    "example": 209,
    "start_line": 3388,
    "end_line": 3392,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\n\"title\" ok\n",
    "html": "<p>&quot;title&quot; ok</p>\n",
    "example": 179,
    "start_line": 3027,
    "end_line": 3032,
    "example": 210,
    "start_line": 3397,
    "end_line": 3402,
    "section": "Link reference definitions"
  },
  {
    "markdown": "    [foo]: /url \"title\"\n\n[foo]\n",
    "html": "<pre><code>[foo]: /url &quot;title&quot;\n</code></pre>\n<p>[foo]</p>\n",
    "example": 180,
    "start_line": 3038,
    "end_line": 3046,
    "example": 211,
    "start_line": 3408,
    "end_line": 3416,
    "section": "Link reference definitions"
  },
  {
    "markdown": "```\n[foo]: /url\n```\n\n[foo]\n",
    "html": "<pre><code>[foo]: /url\n</code></pre>\n<p>[foo]</p>\n",
    "example": 181,
    "start_line": 3052,
    "end_line": 3062,
    "example": 212,
    "start_line": 3422,
    "end_line": 3432,
    "section": "Link reference definitions"
  },
  {
    "markdown": "Foo\n[bar]: /baz\n\n[bar]\n",
    "html": "<p>Foo\n[bar]: /baz</p>\n<p>[bar]</p>\n",
    "example": 182,
    "start_line": 3067,
    "end_line": 3076,
    "example": 213,
    "start_line": 3437,
    "end_line": 3446,
    "section": "Link reference definitions"
  },
  {
    "markdown": "# [Foo]\n[foo]: /url\n> bar\n",
    "html": "<h1><a href=\"/url\">Foo</a></h1>\n<blockquote>\n<p>bar</p>\n</blockquote>\n",
    "example": 183,
    "start_line": 3082,
    "end_line": 3091,
    "example": 214,
    "start_line": 3452,
    "end_line": 3461,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\nbar\n===\n[foo]\n",
    "html": "<h1>bar</h1>\n<p><a href=\"/url\">foo</a></p>\n",
    "example": 184,
    "start_line": 3093,
    "end_line": 3101,
    "example": 215,
    "start_line": 3463,
    "end_line": 3471,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\n===\n[foo]\n",
    "html": "<p>===\n<a href=\"/url\">foo</a></p>\n",
    "example": 185,
    "start_line": 3103,
    "end_line": 3110,
    "example": 216,
    "start_line": 3473,
    "end_line": 3480,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n  \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n",
    "html": "<p><a href=\"/foo-url\" title=\"foo\">foo</a>,\n<a href=\"/bar-url\" title=\"bar\">bar</a>,\n<a href=\"/baz-url\">baz</a></p>\n",
    "example": 186,
    "start_line": 3116,
    "end_line": 3129,
    "example": 217,
    "start_line": 3486,
    "end_line": 3499,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]\n\n> [foo]: /url\n",
    "html": "<p><a href=\"/url\">foo</a></p>\n<blockquote>\n</blockquote>\n",
    "example": 187,
    "start_line": 3137,
    "end_line": 3145,
    "example": 218,
    "start_line": 3507,
    "end_line": 3515,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\n",
    "html": "",
    "example": 188,
    "start_line": 3154,
    "end_line": 3157,
    "section": "Link reference definitions"
  },
  {
    "markdown": "aaa\n\nbbb\n",
    "html": "<p>aaa</p>\n<p>bbb</p>\n",
    "example": 189,
    "start_line": 3171,
    "end_line": 3178,
    "example": 219,
    "start_line": 3529,
    "end_line": 3536,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa\nbbb\n\nccc\nddd\n",
    "html": "<p>aaa\nbbb</p>\n<p>ccc\nddd</p>\n",
    "example": 190,
    "start_line": 3183,
    "end_line": 3194,
    "example": 220,
    "start_line": 3541,
    "end_line": 3552,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa\n\n\nbbb\n",
    "html": "<p>aaa</p>\n<p>bbb</p>\n",
    "example": 191,
    "start_line": 3199,
    "end_line": 3207,
    "example": 221,
    "start_line": 3557,
    "end_line": 3565,
    "section": "Paragraphs"
  },
  {
    "markdown": "  aaa\n bbb\n",
    "html": "<p>aaa\nbbb</p>\n",
    "example": 192,
    "start_line": 3212,
    "end_line": 3218,
    "example": 222,
    "start_line": 3570,
    "end_line": 3576,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa\n             bbb\n                                       ccc\n",
    "html": "<p>aaa\nbbb\nccc</p>\n",
    "example": 193,
    "start_line": 3224,
    "end_line": 3232,
    "example": 223,
    "start_line": 3582,
    "end_line": 3590,
    "section": "Paragraphs"
  },
  {
    "markdown": "   aaa\nbbb\n",
    "html": "<p>aaa\nbbb</p>\n",
    "example": 194,
    "start_line": 3238,
    "end_line": 3244,
    "example": 224,
    "start_line": 3596,
    "end_line": 3602,
    "section": "Paragraphs"
  },
  {
    "markdown": "    aaa\nbbb\n",
    "html": "<pre><code>aaa\n</code></pre>\n<p>bbb</p>\n",
    "example": 195,
    "start_line": 3247,
    "end_line": 3254,
    "example": 225,
    "start_line": 3605,
    "end_line": 3612,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa     \nbbb     \n",
    "html": "<p>aaa<br />\nbbb</p>\n",
    "example": 196,
    "start_line": 3261,
    "end_line": 3267,
    "example": 226,
    "start_line": 3619,
    "end_line": 3625,
    "section": "Paragraphs"
  },
  {
    "markdown": "  \n\naaa\n  \n\n# aaa\n\n  \n",
    "html": "<p>aaa</p>\n<h1>aaa</h1>\n",
    "example": 197,
    "start_line": 3278,
    "end_line": 3290,
    "example": 227,
    "start_line": 3636,
    "end_line": 3648,
    "section": "Blank lines"
  },
  {
    "markdown": "> # Foo\n> bar\n> baz\n",
    "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n",
    "example": 198,
    "start_line": 3344,
    "end_line": 3354,
    "example": 228,
    "start_line": 3704,
    "end_line": 3714,
    "section": "Block quotes"
  },
  {
    "markdown": "># Foo\n>bar\n> baz\n",
    "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n",
    "example": 199,
    "start_line": 3359,
    "end_line": 3369,
    "example": 229,
    "start_line": 3719,
    "end_line": 3729,
    "section": "Block quotes"
  },
  {
    "markdown": "   > # Foo\n   > bar\n > baz\n",
    "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n",
    "example": 200,
    "start_line": 3374,
    "end_line": 3384,
    "example": 230,
    "start_line": 3734,
    "end_line": 3744,
    "section": "Block quotes"
  },
  {
    "markdown": "    > # Foo\n    > bar\n    > baz\n",
    "html": "<pre><code>&gt; # Foo\n&gt; bar\n&gt; baz\n</code></pre>\n",
    "example": 201,
    "start_line": 3389,
    "end_line": 3398,
    "example": 231,
    "start_line": 3749,
    "end_line": 3758,
    "section": "Block quotes"
  },
  {
    "markdown": "> # Foo\n> bar\nbaz\n",
    "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n",
    "example": 202,
    "start_line": 3404,
    "end_line": 3414,
    "example": 232,
    "start_line": 3764,
    "end_line": 3774,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\nbaz\n> foo\n",
    "html": "<blockquote>\n<p>bar\nbaz\nfoo</p>\n</blockquote>\n",
    "example": 203,
    "start_line": 3420,
    "end_line": 3430,
    "example": 233,
    "start_line": 3780,
    "end_line": 3790,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n---\n",
    "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n",
    "example": 204,
    "start_line": 3444,
    "end_line": 3452,
    "example": 234,
    "start_line": 3804,
    "end_line": 3812,
    "section": "Block quotes"
  },
  {
    "markdown": "> - foo\n- bar\n",
    "html": "<blockquote>\n<ul>\n<li>foo</li>\n</ul>\n</blockquote>\n<ul>\n<li>bar</li>\n</ul>\n",
    "example": 205,
    "start_line": 3464,
    "end_line": 3476,
    "example": 235,
    "start_line": 3824,
    "end_line": 3836,
    "section": "Block quotes"
  },
  {
    "markdown": ">     foo\n    bar\n",
    "html": "<blockquote>\n<pre><code>foo\n</code></pre>\n</blockquote>\n<pre><code>bar\n</code></pre>\n",
    "example": 206,
    "start_line": 3482,
    "end_line": 3492,
    "example": 236,
    "start_line": 3842,
    "end_line": 3852,
    "section": "Block quotes"
  },
  {
    "markdown": "> ```\nfoo\n```\n",
    "html": "<blockquote>\n<pre><code></code></pre>\n</blockquote>\n<p>foo</p>\n<pre><code></code></pre>\n",
    "example": 207,
    "start_line": 3495,
    "end_line": 3505,
    "example": 237,
    "start_line": 3855,
    "end_line": 3865,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n    - bar\n",
    "html": "<blockquote>\n<p>foo\n- bar</p>\n</blockquote>\n",
    "example": 208,
    "start_line": 3511,
    "end_line": 3519,
    "example": 238,
    "start_line": 3871,
    "end_line": 3879,
    "section": "Block quotes"
  },
  {
    "markdown": ">\n",
    "html": "<blockquote>\n</blockquote>\n",
    "example": 209,
    "start_line": 3535,
    "end_line": 3540,
    "example": 239,
    "start_line": 3895,
    "end_line": 3900,
    "section": "Block quotes"
  },
  {
    "markdown": ">\n>  \n> \n",
    "html": "<blockquote>\n</blockquote>\n",
    "example": 210,
    "start_line": 3543,
    "end_line": 3550,
    "example": 240,
    "start_line": 3903,
    "end_line": 3910,
    "section": "Block quotes"
  },
  {
    "markdown": ">\n> foo\n>  \n",
    "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n",
    "example": 211,
    "start_line": 3555,
    "end_line": 3563,
    "example": 241,
    "start_line": 3915,
    "end_line": 3923,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n\n> bar\n",
    "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<blockquote>\n<p>bar</p>\n</blockquote>\n",
    "example": 212,
    "start_line": 3568,
    "end_line": 3579,
    "example": 242,
    "start_line": 3928,
    "end_line": 3939,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n> bar\n",
    "html": "<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n",
    "example": 213,
    "start_line": 3590,
    "end_line": 3598,
    "example": 243,
    "start_line": 3950,
    "end_line": 3958,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n>\n> bar\n",
    "html": "<blockquote>\n<p>foo</p>\n<p>bar</p>\n</blockquote>\n",
    "example": 214,
    "start_line": 3603,
    "end_line": 3612,
    "example": 244,
    "start_line": 3963,
    "end_line": 3972,
    "section": "Block quotes"
  },
  {
    "markdown": "foo\n> bar\n",
    "html": "<p>foo</p>\n<blockquote>\n<p>bar</p>\n</blockquote>\n",
    "example": 215,
    "start_line": 3617,
    "end_line": 3625,
    "example": 245,
    "start_line": 3977,
    "end_line": 3985,
    "section": "Block quotes"
  },
  {
    "markdown": "> aaa\n***\n> bbb\n",
    "html": "<blockquote>\n<p>aaa</p>\n</blockquote>\n<hr />\n<blockquote>\n<p>bbb</p>\n</blockquote>\n",
    "example": 216,
    "start_line": 3631,
    "end_line": 3643,
    "example": 246,
    "start_line": 3991,
    "end_line": 4003,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\nbaz\n",
    "html": "<blockquote>\n<p>bar\nbaz</p>\n</blockquote>\n",
    "example": 217,
    "start_line": 3649,
    "end_line": 3657,
    "example": 247,
    "start_line": 4009,
    "end_line": 4017,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\n\nbaz\n",
    "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n",
    "example": 218,
    "start_line": 3660,
    "end_line": 3669,
    "example": 248,
    "start_line": 4020,
    "end_line": 4029,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\n>\nbaz\n",
    "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n",
    "example": 219,
    "start_line": 3672,
    "end_line": 3681,
    "example": 249,
    "start_line": 4032,
    "end_line": 4041,
    "section": "Block quotes"
  },
  {
    "markdown": "> > > foo\nbar\n",
    "html": "<blockquote>\n<blockquote>\n<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n</blockquote>\n</blockquote>\n",
    "example": 220,
    "start_line": 3688,
    "end_line": 3700,
    "example": 250,
    "start_line": 4048,
    "end_line": 4060,
    "section": "Block quotes"
  },
  {
    "markdown": ">>> foo\n> bar\n>>baz\n",
    "html": "<blockquote>\n<blockquote>\n<blockquote>\n<p>foo\nbar\nbaz</p>\n</blockquote>\n</blockquote>\n</blockquote>\n",
    "example": 221,
    "start_line": 3703,
    "end_line": 3717,
    "example": 251,
    "start_line": 4063,
    "end_line": 4077,
    "section": "Block quotes"
  },
  {
    "markdown": ">     code\n\n>    not code\n",
    "html": "<blockquote>\n<pre><code>code\n</code></pre>\n</blockquote>\n<blockquote>\n<p>not code</p>\n</blockquote>\n",
    "example": 222,
    "start_line": 3725,
    "end_line": 3737,
    "example": 252,
    "start_line": 4085,
    "end_line": 4097,
    "section": "Block quotes"
  },
  {
    "markdown": "A paragraph\nwith two lines.\n\n    indented code\n\n> A block quote.\n",
    "html": "<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n",
    "example": 223,
    "start_line": 3779,
    "end_line": 3794,
    "example": 253,
    "start_line": 4139,
    "end_line": 4154,
    "section": "List items"
  },
  {
    "markdown": "1.  A paragraph\n    with two lines.\n\n        indented code\n\n    > A block quote.\n",
    "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n",
    "example": 224,
    "start_line": 3801,
    "end_line": 3820,
    "example": 254,
    "start_line": 4161,
    "end_line": 4180,
    "section": "List items"
  },
  {
    "markdown": "- one\n\n two\n",
    "html": "<ul>\n<li>one</li>\n</ul>\n<p>two</p>\n",
    "example": 225,
    "start_line": 3834,
    "end_line": 3843,
    "example": 255,
    "start_line": 4194,
    "end_line": 4203,
    "section": "List items"
  },
  {
    "markdown": "- one\n\n  two\n",
    "html": "<ul>\n<li>\n<p>one</p>\n<p>two</p>\n</li>\n</ul>\n",
    "example": 226,
    "start_line": 3846,
    "end_line": 3857,
    "example": 256,
    "start_line": 4206,
    "end_line": 4217,
    "section": "List items"
  },
  {
    "markdown": " -    one\n\n     two\n",
    "html": "<ul>\n<li>one</li>\n</ul>\n<pre><code> two\n</code></pre>\n",
    "example": 227,
    "start_line": 3860,
    "end_line": 3870,
    "example": 257,
    "start_line": 4220,
    "end_line": 4230,
    "section": "List items"
  },
  {
    "markdown": " -    one\n\n      two\n",
    "html": "<ul>\n<li>\n<p>one</p>\n<p>two</p>\n</li>\n</ul>\n",
    "example": 228,
    "start_line": 3873,
    "end_line": 3884,
    "example": 258,
    "start_line": 4233,
    "end_line": 4244,
    "section": "List items"
  },
  {
    "markdown": "   > > 1.  one\n>>\n>>     two\n",
    "html": "<blockquote>\n<blockquote>\n<ol>\n<li>\n<p>one</p>\n<p>two</p>\n</li>\n</ol>\n</blockquote>\n</blockquote>\n",
    "example": 229,
    "start_line": 3895,
    "end_line": 3910,
    "example": 259,
    "start_line": 4255,
    "end_line": 4270,
    "section": "List items"
  },
  {
    "markdown": ">>- one\n>>\n  >  > two\n",
    "html": "<blockquote>\n<blockquote>\n<ul>\n<li>one</li>\n</ul>\n<p>two</p>\n</blockquote>\n</blockquote>\n",
    "example": 230,
    "start_line": 3922,
    "end_line": 3935,
    "example": 260,
    "start_line": 4282,
    "end_line": 4295,
    "section": "List items"
  },
  {
    "markdown": "-one\n\n2.two\n",
    "html": "<p>-one</p>\n<p>2.two</p>\n",
    "example": 231,
    "start_line": 3941,
    "end_line": 3948,
    "example": 261,
    "start_line": 4301,
    "end_line": 4308,
    "section": "List items"
  },
  {
    "markdown": "- foo\n\n\n  bar\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n",
    "example": 232,
    "start_line": 3954,
    "end_line": 3966,
    "example": 262,
    "start_line": 4314,
    "end_line": 4326,
    "section": "List items"
  },
  {
    "markdown": "1.  foo\n\n    ```\n    bar\n    ```\n\n    baz\n\n    > bam\n",
    "html": "<ol>\n<li>\n<p>foo</p>\n<pre><code>bar\n</code></pre>\n<p>baz</p>\n<blockquote>\n<p>bam</p>\n</blockquote>\n</li>\n</ol>\n",
    "example": 233,
    "start_line": 3971,
    "end_line": 3993,
    "example": 263,
    "start_line": 4331,
    "end_line": 4353,
    "section": "List items"
  },
  {
    "markdown": "- Foo\n\n      bar\n\n\n      baz\n",
    "html": "<ul>\n<li>\n<p>Foo</p>\n<pre><code>bar\n\n\nbaz\n</code></pre>\n</li>\n</ul>\n",
    "example": 234,
    "start_line": 3999,
    "end_line": 4017,
    "example": 264,
    "start_line": 4359,
    "end_line": 4377,
    "section": "List items"
  },
  {
    "markdown": "123456789. ok\n",
    "html": "<ol start=\"123456789\">\n<li>ok</li>\n</ol>\n",
    "example": 235,
    "start_line": 4021,
    "end_line": 4027,
    "example": 265,
    "start_line": 4381,
    "end_line": 4387,
    "section": "List items"
  },
  {
    "markdown": "1234567890. not ok\n",
    "html": "<p>1234567890. not ok</p>\n",
    "example": 236,
    "start_line": 4030,
    "end_line": 4034,
    "example": 266,
    "start_line": 4390,
    "end_line": 4394,
    "section": "List items"
  },
  {
    "markdown": "0. ok\n",
    "html": "<ol start=\"0\">\n<li>ok</li>\n</ol>\n",
    "example": 237,
    "start_line": 4039,
    "end_line": 4045,
    "example": 267,
    "start_line": 4399,
    "end_line": 4405,
    "section": "List items"
  },
  {
    "markdown": "003. ok\n",
    "html": "<ol start=\"3\">\n<li>ok</li>\n</ol>\n",
    "example": 238,
    "start_line": 4048,
    "end_line": 4054,
    "example": 268,
    "start_line": 4408,
    "end_line": 4414,
    "section": "List items"
  },
  {
    "markdown": "-1. not ok\n",
    "html": "<p>-1. not ok</p>\n",
    "example": 239,
    "start_line": 4059,
    "end_line": 4063,
    "example": 269,
    "start_line": 4419,
    "end_line": 4423,
    "section": "List items"
  },
  {
    "markdown": "- foo\n\n      bar\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<pre><code>bar\n</code></pre>\n</li>\n</ul>\n",
    "example": 240,
    "start_line": 4082,
    "end_line": 4094,
    "example": 270,
    "start_line": 4442,
    "end_line": 4454,
    "section": "List items"
  },
  {
    "markdown": "  10.  foo\n\n           bar\n",
    "html": "<ol start=\"10\">\n<li>\n<p>foo</p>\n<pre><code>bar\n</code></pre>\n</li>\n</ol>\n",
    "example": 241,
    "start_line": 4099,
    "end_line": 4111,
    "example": 271,
    "start_line": 4459,
    "end_line": 4471,
    "section": "List items"
  },
  {
    "markdown": "    indented code\n\nparagraph\n\n    more code\n",
    "html": "<pre><code>indented code\n</code></pre>\n<p>paragraph</p>\n<pre><code>more code\n</code></pre>\n",
    "example": 242,
    "start_line": 4118,
    "end_line": 4130,
    "example": 272,
    "start_line": 4478,
    "end_line": 4490,
    "section": "List items"
  },
  {
    "markdown": "1.     indented code\n\n   paragraph\n\n       more code\n",
    "html": "<ol>\n<li>\n<pre><code>indented code\n</code></pre>\n<p>paragraph</p>\n<pre><code>more code\n</code></pre>\n</li>\n</ol>\n",
    "example": 243,
    "start_line": 4133,
    "end_line": 4149,
    "example": 273,
    "start_line": 4493,
    "end_line": 4509,
    "section": "List items"
  },
  {
    "markdown": "1.      indented code\n\n   paragraph\n\n       more code\n",
    "html": "<ol>\n<li>\n<pre><code> indented code\n</code></pre>\n<p>paragraph</p>\n<pre><code>more code\n</code></pre>\n</li>\n</ol>\n",
    "example": 244,
    "start_line": 4155,
    "end_line": 4171,
    "example": 274,
    "start_line": 4515,
    "end_line": 4531,
    "section": "List items"
  },
  {
    "markdown": "   foo\n\nbar\n",
    "html": "<p>foo</p>\n<p>bar</p>\n",
    "example": 245,
    "start_line": 4182,
    "end_line": 4189,
    "example": 275,
    "start_line": 4542,
    "end_line": 4549,
    "section": "List items"
  },
  {
    "markdown": "-    foo\n\n  bar\n",
    "html": "<ul>\n<li>foo</li>\n</ul>\n<p>bar</p>\n",
    "example": 246,
    "start_line": 4192,
    "end_line": 4201,
    "example": 276,
    "start_line": 4552,
    "end_line": 4561,
    "section": "List items"
  },
  {
    "markdown": "-  foo\n\n   bar\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n",
    "example": 247,
    "start_line": 4209,
    "end_line": 4220,
    "example": 277,
    "start_line": 4569,
    "end_line": 4580,
    "section": "List items"
  },
  {
    "markdown": "-\n  foo\n-\n  ```\n  bar\n  ```\n-\n      baz\n",
    "html": "<ul>\n<li>foo</li>\n<li>\n<pre><code>bar\n</code></pre>\n</li>\n<li>\n<pre><code>baz\n</code></pre>\n</li>\n</ul>\n",
    "example": 248,
    "start_line": 4237,
    "end_line": 4258,
    "example": 278,
    "start_line": 4596,
    "end_line": 4617,
    "section": "List items"
  },
  {
    "markdown": "-   \n  foo\n",
    "html": "<ul>\n<li>foo</li>\n</ul>\n",
    "example": 249,
    "start_line": 4263,
    "end_line": 4270,
    "example": 279,
    "start_line": 4622,
    "end_line": 4629,
    "section": "List items"
  },
  {
    "markdown": "-\n\n  foo\n",
    "html": "<ul>\n<li></li>\n</ul>\n<p>foo</p>\n",
    "example": 250,
    "start_line": 4277,
    "end_line": 4286,
    "example": 280,
    "start_line": 4636,
    "end_line": 4645,
    "section": "List items"
  },
  {
    "markdown": "- foo\n-\n- bar\n",
    "html": "<ul>\n<li>foo</li>\n<li></li>\n<li>bar</li>\n</ul>\n",
    "example": 251,
    "start_line": 4291,
    "end_line": 4301,
    "example": 281,
    "start_line": 4650,
    "end_line": 4660,
    "section": "List items"
  },
  {
    "markdown": "- foo\n-   \n- bar\n",
    "html": "<ul>\n<li>foo</li>\n<li></li>\n<li>bar</li>\n</ul>\n",
    "example": 252,
    "start_line": 4306,
    "end_line": 4316,
    "example": 282,
    "start_line": 4665,
    "end_line": 4675,
    "section": "List items"
  },
  {
    "markdown": "1. foo\n2.\n3. bar\n",
    "html": "<ol>\n<li>foo</li>\n<li></li>\n<li>bar</li>\n</ol>\n",
    "example": 253,
    "start_line": 4321,
    "end_line": 4331,
    "example": 283,
    "start_line": 4680,
    "end_line": 4690,
    "section": "List items"
  },
  {
    "markdown": "*\n",
    "html": "<ul>\n<li></li>\n</ul>\n",
    "example": 254,
    "start_line": 4336,
    "end_line": 4342,
    "example": 284,
    "start_line": 4695,
    "end_line": 4701,
    "section": "List items"
  },
  {
    "markdown": "foo\n*\n\nfoo\n1.\n",
    "html": "<p>foo\n*</p>\n<p>foo\n1.</p>\n",
    "example": 255,
    "start_line": 4346,
    "end_line": 4357,
    "example": 285,
    "start_line": 4705,
    "end_line": 4716,
    "section": "List items"
  },
  {
    "markdown": " 1.  A paragraph\n     with two lines.\n\n         indented code\n\n     > A block quote.\n",
    "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n",
    "example": 256,
    "start_line": 4368,
    "end_line": 4387,
    "example": 286,
    "start_line": 4727,
    "end_line": 4746,
    "section": "List items"
  },
  {
    "markdown": "  1.  A paragraph\n      with two lines.\n\n          indented code\n\n      > A block quote.\n",
    "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n",
    "example": 257,
    "start_line": 4392,
    "end_line": 4411,
    "example": 287,
    "start_line": 4751,
    "end_line": 4770,
    "section": "List items"
  },
  {
    "markdown": "   1.  A paragraph\n       with two lines.\n\n           indented code\n\n       > A block quote.\n",
    "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n",
    "example": 258,
    "start_line": 4416,
    "end_line": 4435,
    "example": 288,
    "start_line": 4775,
    "end_line": 4794,
    "section": "List items"
  },
  {
    "markdown": "    1.  A paragraph\n        with two lines.\n\n            indented code\n\n        > A block quote.\n",
    "html": "<pre><code>1.  A paragraph\n    with two lines.\n\n        indented code\n\n    &gt; A block quote.\n</code></pre>\n",
    "example": 259,
    "start_line": 4440,
    "end_line": 4455,
    "example": 289,
    "start_line": 4799,
    "end_line": 4814,
    "section": "List items"
  },
  {
    "markdown": "  1.  A paragraph\nwith two lines.\n\n          indented code\n\n      > A block quote.\n",
    "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n",
    "example": 260,
    "start_line": 4470,
    "end_line": 4489,
    "example": 290,
    "start_line": 4829,
    "end_line": 4848,
    "section": "List items"
  },
  {
    "markdown": "  1.  A paragraph\n    with two lines.\n",
    "html": "<ol>\n<li>A paragraph\nwith two lines.</li>\n</ol>\n",
    "example": 261,
    "start_line": 4494,
    "end_line": 4502,
    "example": 291,
    "start_line": 4853,
    "end_line": 4861,
    "section": "List items"
  },
  {
    "markdown": "> 1. > Blockquote\ncontinued here.\n",
    "html": "<blockquote>\n<ol>\n<li>\n<blockquote>\n<p>Blockquote\ncontinued here.</p>\n</blockquote>\n</li>\n</ol>\n</blockquote>\n",
    "example": 262,
    "start_line": 4507,
    "end_line": 4521,
    "example": 292,
    "start_line": 4866,
    "end_line": 4880,
    "section": "List items"
  },
  {
    "markdown": "> 1. > Blockquote\n> continued here.\n",
    "html": "<blockquote>\n<ol>\n<li>\n<blockquote>\n<p>Blockquote\ncontinued here.</p>\n</blockquote>\n</li>\n</ol>\n</blockquote>\n",
    "example": 263,
    "start_line": 4524,
    "end_line": 4538,
    "example": 293,
    "start_line": 4883,
    "end_line": 4897,
    "section": "List items"
  },
  {
    "markdown": "- foo\n  - bar\n    - baz\n      - boo\n",
    "html": "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>baz\n<ul>\n<li>boo</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n",
    "example": 264,
    "start_line": 4552,
    "end_line": 4573,
    "example": 294,
    "start_line": 4911,
    "end_line": 4932,
    "section": "List items"
  },
  {
    "markdown": "- foo\n - bar\n  - baz\n   - boo\n",
    "html": "<ul>\n<li>foo</li>\n<li>bar</li>\n<li>baz</li>\n<li>boo</li>\n</ul>\n",
    "example": 265,
    "start_line": 4578,
    "end_line": 4590,
    "example": 295,
    "start_line": 4937,
    "end_line": 4949,
    "section": "List items"
  },
  {
    "markdown": "10) foo\n    - bar\n",
    "html": "<ol start=\"10\">\n<li>foo\n<ul>\n<li>bar</li>\n</ul>\n</li>\n</ol>\n",
    "example": 266,
    "start_line": 4595,
    "end_line": 4606,
    "example": 296,
    "start_line": 4954,
    "end_line": 4965,
    "section": "List items"
  },
  {
    "markdown": "10) foo\n   - bar\n",
    "html": "<ol start=\"10\">\n<li>foo</li>\n</ol>\n<ul>\n<li>bar</li>\n</ul>\n",
    "example": 267,
    "start_line": 4611,
    "end_line": 4621,
    "example": 297,
    "start_line": 4970,
    "end_line": 4980,
    "section": "List items"
  },
  {
    "markdown": "- - foo\n",
    "html": "<ul>\n<li>\n<ul>\n<li>foo</li>\n</ul>\n</li>\n</ul>\n",
    "example": 268,
    "start_line": 4626,
    "end_line": 4636,
    "example": 298,
    "start_line": 4985,
    "end_line": 4995,
    "section": "List items"
  },
  {
    "markdown": "1. - 2. foo\n",
    "html": "<ol>\n<li>\n<ul>\n<li>\n<ol start=\"2\">\n<li>foo</li>\n</ol>\n</li>\n</ul>\n</li>\n</ol>\n",
    "example": 269,
    "start_line": 4639,
    "end_line": 4653,
    "example": 299,
    "start_line": 4998,
    "end_line": 5012,
    "section": "List items"
  },
  {
    "markdown": "- # Foo\n- Bar\n  ---\n  baz\n",
    "html": "<ul>\n<li>\n<h1>Foo</h1>\n</li>\n<li>\n<h2>Bar</h2>\nbaz</li>\n</ul>\n",
    "example": 270,
    "start_line": 4658,
    "end_line": 4672,
    "example": 300,
    "start_line": 5017,
    "end_line": 5031,
    "section": "List items"
  },
  {
    "markdown": "- foo\n- bar\n+ baz\n",
    "html": "<ul>\n<li>foo</li>\n<li>bar</li>\n</ul>\n<ul>\n<li>baz</li>\n</ul>\n",
    "example": 271,
    "start_line": 4894,
    "end_line": 4906,
    "example": 301,
    "start_line": 5253,
    "end_line": 5265,
    "section": "Lists"
  },
  {
    "markdown": "1. foo\n2. bar\n3) baz\n",
    "html": "<ol>\n<li>foo</li>\n<li>bar</li>\n</ol>\n<ol start=\"3\">\n<li>baz</li>\n</ol>\n",
    "example": 272,
    "start_line": 4909,
    "end_line": 4921,
    "example": 302,
    "start_line": 5268,
    "end_line": 5280,
    "section": "Lists"
  },
  {
    "markdown": "Foo\n- bar\n- baz\n",
    "html": "<p>Foo</p>\n<ul>\n<li>bar</li>\n<li>baz</li>\n</ul>\n",
    "example": 273,
    "start_line": 4928,
    "end_line": 4938,
    "example": 303,
    "start_line": 5287,
    "end_line": 5297,
    "section": "Lists"
  },
  {
    "markdown": "The number of windows in my house is\n14.  The number of doors is 6.\n",
    "html": "<p>The number of windows in my house is\n14.  The number of doors is 6.</p>\n",
    "example": 274,
    "start_line": 5005,
    "end_line": 5011,
    "example": 304,
    "start_line": 5364,
    "end_line": 5370,
    "section": "Lists"
  },
  {
    "markdown": "The number of windows in my house is\n1.  The number of doors is 6.\n",
    "html": "<p>The number of windows in my house is</p>\n<ol>\n<li>The number of doors is 6.</li>\n</ol>\n",
    "example": 275,
    "start_line": 5015,
    "end_line": 5023,
    "example": 305,
    "start_line": 5374,
    "end_line": 5382,
    "section": "Lists"
  },
  {
    "markdown": "- foo\n\n- bar\n\n\n- baz\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n</li>\n<li>\n<p>bar</p>\n</li>\n<li>\n<p>baz</p>\n</li>\n</ul>\n",
    "example": 276,
    "start_line": 5029,
    "end_line": 5048,
    "example": 306,
    "start_line": 5388,
    "end_line": 5407,
    "section": "Lists"
  },
  {
    "markdown": "- foo\n  - bar\n    - baz\n\n\n      bim\n",
    "html": "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>\n<p>baz</p>\n<p>bim</p>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n",
    "example": 277,
    "start_line": 5050,
    "end_line": 5072,
    "example": 307,
    "start_line": 5409,
    "end_line": 5431,
    "section": "Lists"
  },
  {
    "markdown": "- foo\n- bar\n\n<!-- -->\n\n- baz\n- bim\n",
    "html": "<ul>\n<li>foo</li>\n<li>bar</li>\n</ul>\n<!-- -->\n<ul>\n<li>baz</li>\n<li>bim</li>\n</ul>\n",
    "example": 278,
    "start_line": 5080,
    "end_line": 5098,
    "example": 308,
    "start_line": 5439,
    "end_line": 5457,
    "section": "Lists"
  },
  {
    "markdown": "-   foo\n\n    notcode\n\n-   foo\n\n<!-- -->\n\n    code\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<p>notcode</p>\n</li>\n<li>\n<p>foo</p>\n</li>\n</ul>\n<!-- -->\n<pre><code>code\n</code></pre>\n",
    "example": 279,
    "start_line": 5101,
    "end_line": 5124,
    "example": 309,
    "start_line": 5460,
    "end_line": 5483,
    "section": "Lists"
  },
  {
    "markdown": "- a\n - b\n  - c\n   - d\n  - e\n - f\n- g\n",
    "html": "<ul>\n<li>a</li>\n<li>b</li>\n<li>c</li>\n<li>d</li>\n<li>e</li>\n<li>f</li>\n<li>g</li>\n</ul>\n",
    "example": 280,
    "start_line": 5132,
    "end_line": 5150,
    "example": 310,
    "start_line": 5491,
    "end_line": 5509,
    "section": "Lists"
  },
  {
    "markdown": "1. a\n\n  2. b\n\n   3. c\n",
    "html": "<ol>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n<li>\n<p>c</p>\n</li>\n</ol>\n",
    "example": 281,
    "start_line": 5153,
    "end_line": 5171,
    "example": 311,
    "start_line": 5512,
    "end_line": 5530,
    "section": "Lists"
  },
  {
    "markdown": "- a\n - b\n  - c\n   - d\n    - e\n",
    "html": "<ul>\n<li>a</li>\n<li>b</li>\n<li>c</li>\n<li>d\n- e</li>\n</ul>\n",
    "example": 282,
    "start_line": 5177,
    "end_line": 5191,
    "example": 312,
    "start_line": 5536,
    "end_line": 5550,
    "section": "Lists"
  },
  {
    "markdown": "1. a\n\n  2. b\n\n    3. c\n",
    "html": "<ol>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n</ol>\n<pre><code>3. c\n</code></pre>\n",
    "example": 283,
    "start_line": 5197,
    "end_line": 5214,
    "example": 313,
    "start_line": 5556,
    "end_line": 5573,
    "section": "Lists"
  },
  {
    "markdown": "- a\n- b\n\n- c\n",
    "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n<li>\n<p>c</p>\n</li>\n</ul>\n",
    "example": 284,
    "start_line": 5220,
    "end_line": 5237,
    "example": 314,
    "start_line": 5579,
    "end_line": 5596,
    "section": "Lists"
  },
  {
    "markdown": "* a\n*\n\n* c\n",
    "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li></li>\n<li>\n<p>c</p>\n</li>\n</ul>\n",
    "example": 285,
    "start_line": 5242,
    "end_line": 5257,
    "example": 315,
    "start_line": 5601,
    "end_line": 5616,
    "section": "Lists"
  },
  {
    "markdown": "- a\n- b\n\n  c\n- d\n",
    "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n<p>c</p>\n</li>\n<li>\n<p>d</p>\n</li>\n</ul>\n",
    "example": 286,
    "start_line": 5264,
    "end_line": 5283,
    "example": 316,
    "start_line": 5623,
    "end_line": 5642,
    "section": "Lists"
  },
  {
    "markdown": "- a\n- b\n\n  [ref]: /url\n- d\n",
    "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n<li>\n<p>d</p>\n</li>\n</ul>\n",
    "example": 287,
    "start_line": 5286,
    "end_line": 5304,
    "example": 317,
    "start_line": 5645,
    "end_line": 5663,
    "section": "Lists"
  },
  {
    "markdown": "- a\n- ```\n  b\n\n\n  ```\n- c\n",
    "html": "<ul>\n<li>a</li>\n<li>\n<pre><code>b\n\n\n</code></pre>\n</li>\n<li>c</li>\n</ul>\n",
    "example": 288,
    "start_line": 5309,
    "end_line": 5328,
    "example": 318,
    "start_line": 5668,
    "end_line": 5687,
    "section": "Lists"
  },
  {
    "markdown": "- a\n  - b\n\n    c\n- d\n",
    "html": "<ul>\n<li>a\n<ul>\n<li>\n<p>b</p>\n<p>c</p>\n</li>\n</ul>\n</li>\n<li>d</li>\n</ul>\n",
    "example": 289,
    "start_line": 5335,
    "end_line": 5353,
    "example": 319,
    "start_line": 5694,
    "end_line": 5712,
    "section": "Lists"
  },
  {
    "markdown": "* a\n  > b\n  >\n* c\n",
    "html": "<ul>\n<li>a\n<blockquote>\n<p>b</p>\n</blockquote>\n</li>\n<li>c</li>\n</ul>\n",
    "example": 290,
    "start_line": 5359,
    "end_line": 5373,
    "example": 320,
    "start_line": 5718,
    "end_line": 5732,
    "section": "Lists"
  },
  {
    "markdown": "- a\n  > b\n  ```\n  c\n  ```\n- d\n",
    "html": "<ul>\n<li>a\n<blockquote>\n<p>b</p>\n</blockquote>\n<pre><code>c\n</code></pre>\n</li>\n<li>d</li>\n</ul>\n",
    "example": 291,
    "start_line": 5379,
    "end_line": 5397,
    "example": 321,
    "start_line": 5738,
    "end_line": 5756,
    "section": "Lists"
  },
  {
    "markdown": "- a\n",
    "html": "<ul>\n<li>a</li>\n</ul>\n",
    "example": 292,
    "start_line": 5402,
    "end_line": 5408,
    "example": 322,
    "start_line": 5761,
    "end_line": 5767,
    "section": "Lists"
  },
  {
    "markdown": "- a\n  - b\n",
    "html": "<ul>\n<li>a\n<ul>\n<li>b</li>\n</ul>\n</li>\n</ul>\n",
    "example": 293,
    "start_line": 5411,
    "end_line": 5422,
    "example": 323,
    "start_line": 5770,
    "end_line": 5781,
    "section": "Lists"
  },
  {
    "markdown": "1. ```\n   foo\n   ```\n\n   bar\n",
    "html": "<ol>\n<li>\n<pre><code>foo\n</code></pre>\n<p>bar</p>\n</li>\n</ol>\n",
    "example": 294,
    "start_line": 5428,
    "end_line": 5442,
    "example": 324,
    "start_line": 5787,
    "end_line": 5801,
    "section": "Lists"
  },
  {
    "markdown": "* foo\n  * bar\n\n  baz\n",
    "html": "<ul>\n<li>\n<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n<p>baz</p>\n</li>\n</ul>\n",
    "example": 295,
    "start_line": 5447,
    "end_line": 5462,
    "example": 325,
    "start_line": 5806,
    "end_line": 5821,
    "section": "Lists"
  },
  {
    "markdown": "- a\n  - b\n  - c\n\n- d\n  - e\n  - f\n",
    "html": "<ul>\n<li>\n<p>a</p>\n<ul>\n<li>b</li>\n<li>c</li>\n</ul>\n</li>\n<li>\n<p>d</p>\n<ul>\n<li>e</li>\n<li>f</li>\n</ul>\n</li>\n</ul>\n",
    "example": 296,
    "start_line": 5465,
    "end_line": 5490,
    "example": 326,
    "start_line": 5824,
    "end_line": 5849,
    "section": "Lists"
  },
  {
    "markdown": "`hi`lo`\n",
    "html": "<p><code>hi</code>lo`</p>\n",
    "example": 297,
    "start_line": 5499,
    "end_line": 5503,
    "example": 327,
    "start_line": 5858,
    "end_line": 5862,
    "section": "Inlines"
  },
  {
    "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n",
    "html": "<p>!&quot;#$%&amp;'()*+,-./:;&lt;=&gt;?@[\\]^_`{|}~</p>\n",
    "example": 298,
    "start_line": 5513,
    "end_line": 5517,
    "section": "Backslash escapes"
  },
  {
    "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n",
    "html": "<p>\\\t\\A\\a\\ \\3\\φ\\«</p>\n",
    "example": 299,
    "start_line": 5523,
    "end_line": 5527,
    "section": "Backslash escapes"
  },
  {
    "markdown": "\\*not emphasized*\n\\<br/> not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\&ouml; not a character entity\n",
    "html": "<p>*not emphasized*\n&lt;br/&gt; not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url &quot;not a reference&quot;\n&amp;ouml; not a character entity</p>\n",
    "example": 300,
    "start_line": 5533,
    "end_line": 5553,
    "section": "Backslash escapes"
  },
  {
    "markdown": "\\\\*emphasis*\n",
    "html": "<p>\\<em>emphasis</em></p>\n",
    "example": 301,
    "start_line": 5558,
    "end_line": 5562,
    "section": "Backslash escapes"
  },
  {
    "markdown": "foo\\\nbar\n",
    "html": "<p>foo<br />\nbar</p>\n",
    "example": 302,
    "start_line": 5567,
    "end_line": 5573,
    "section": "Backslash escapes"
  },
  {
    "markdown": "`` \\[\\` ``\n",
    "html": "<p><code>\\[\\`</code></p>\n",
    "example": 303,
    "start_line": 5579,
    "end_line": 5583,
    "section": "Backslash escapes"
  },
  {
    "markdown": "    \\[\\]\n",
    "html": "<pre><code>\\[\\]\n</code></pre>\n",
    "example": 304,
    "start_line": 5586,
    "end_line": 5591,
    "section": "Backslash escapes"
  },
  {
    "markdown": "~~~\n\\[\\]\n~~~\n",
    "html": "<pre><code>\\[\\]\n</code></pre>\n",
    "example": 305,
    "start_line": 5594,
    "end_line": 5601,
    "section": "Backslash escapes"
  },
  {
    "markdown": "<http://example.com?find=\\*>\n",
    "html": "<p><a href=\"http://example.com?find=%5C*\">http://example.com?find=\\*</a></p>\n",
    "example": 306,
    "start_line": 5604,
    "end_line": 5608,
    "section": "Backslash escapes"
  },
  {
    "markdown": "<a href=\"/bar\\/)\">\n",
    "html": "<a href=\"/bar\\/)\">\n",
    "example": 307,
    "start_line": 5611,
    "end_line": 5615,
    "section": "Backslash escapes"
  },
  {
    "markdown": "[foo](/bar\\* \"ti\\*tle\")\n",
    "html": "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n",
    "example": 308,
    "start_line": 5621,
    "end_line": 5625,
    "section": "Backslash escapes"
  },
  {
    "markdown": "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n",
    "html": "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n",
    "example": 309,
    "start_line": 5628,
    "end_line": 5634,
    "section": "Backslash escapes"
  },
  {
    "markdown": "``` foo\\+bar\nfoo\n```\n",
    "html": "<pre><code class=\"language-foo+bar\">foo\n</code></pre>\n",
    "example": 310,
    "start_line": 5637,
    "end_line": 5644,
    "section": "Backslash escapes"
  },
  {
    "markdown": "&nbsp; &amp; &copy; &AElig; &Dcaron;\n&frac34; &HilbertSpace; &DifferentialD;\n&ClockwiseContourIntegral; &ngE;\n",
    "html": "<p>  &amp; © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸</p>\n",
    "example": 311,
    "start_line": 5674,
    "end_line": 5682,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#35; &#1234; &#992; &#0;\n",
    "html": "<p># Ӓ Ϡ �</p>\n",
    "example": 312,
    "start_line": 5693,
    "end_line": 5697,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#X22; &#XD06; &#xcab;\n",
    "html": "<p>&quot; ആ ಫ</p>\n",
    "example": 313,
    "start_line": 5706,
    "end_line": 5710,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&nbsp &x; &#; &#x;\n&#987654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n",
    "html": "<p>&amp;nbsp &amp;x; &amp;#; &amp;#x;\n&amp;#987654321;\n&amp;#abcdef0;\n&amp;ThisIsNotDefined; &amp;hi?;</p>\n",
    "example": 314,
    "start_line": 5715,
    "end_line": 5725,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&copy\n",
    "html": "<p>&amp;copy</p>\n",
    "example": 315,
    "start_line": 5732,
    "end_line": 5736,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&MadeUpEntity;\n",
    "html": "<p>&amp;MadeUpEntity;</p>\n",
    "example": 316,
    "start_line": 5742,
    "end_line": 5746,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "<a href=\"&ouml;&ouml;.html\">\n",
    "html": "<a href=\"&ouml;&ouml;.html\">\n",
    "example": 317,
    "start_line": 5753,
    "end_line": 5757,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "[foo](/f&ouml;&ouml; \"f&ouml;&ouml;\")\n",
    "html": "<p><a href=\"/f%C3%B6%C3%B6\" title=\"föö\">foo</a></p>\n",
    "example": 318,
    "start_line": 5760,
    "end_line": 5764,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "[foo]\n\n[foo]: /f&ouml;&ouml; \"f&ouml;&ouml;\"\n",
    "html": "<p><a href=\"/f%C3%B6%C3%B6\" title=\"föö\">foo</a></p>\n",
    "example": 319,
    "start_line": 5767,
    "end_line": 5773,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "``` f&ouml;&ouml;\nfoo\n```\n",
    "html": "<pre><code class=\"language-föö\">foo\n</code></pre>\n",
    "example": 320,
    "start_line": 5776,
    "end_line": 5783,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "`f&ouml;&ouml;`\n",
    "html": "<p><code>f&amp;ouml;&amp;ouml;</code></p>\n",
    "example": 321,
    "start_line": 5789,
    "end_line": 5793,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "    f&ouml;f&ouml;\n",
    "html": "<pre><code>f&amp;ouml;f&amp;ouml;\n</code></pre>\n",
    "example": 322,
    "start_line": 5796,
    "end_line": 5801,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#42;foo&#42;\n*foo*\n",
    "html": "<p>*foo*\n<em>foo</em></p>\n",
    "example": 323,
    "start_line": 5808,
    "end_line": 5814,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#42; foo\n\n* foo\n",
    "html": "<p>* foo</p>\n<ul>\n<li>foo</li>\n</ul>\n",
    "example": 324,
    "start_line": 5816,
    "end_line": 5825,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "foo&#10;&#10;bar\n",
    "html": "<p>foo\n\nbar</p>\n",
    "example": 325,
    "start_line": 5827,
    "end_line": 5833,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "&#9;foo\n",
    "html": "<p>\tfoo</p>\n",
    "example": 326,
    "start_line": 5835,
    "end_line": 5839,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "[a](url &quot;tit&quot;)\n",
    "html": "<p>[a](url &quot;tit&quot;)</p>\n",
    "example": 327,
    "start_line": 5842,
    "end_line": 5846,
    "section": "Entity and numeric character references"
  },
  {
    "markdown": "`foo`\n",
    "html": "<p><code>foo</code></p>\n",
    "example": 328,
    "start_line": 5870,
    "end_line": 5874,
    "start_line": 5890,
    "end_line": 5894,
    "section": "Code spans"
  },
  {
    "markdown": "`` foo ` bar ``\n",
    "html": "<p><code>foo ` bar</code></p>\n",
    "example": 329,
    "start_line": 5881,
    "end_line": 5885,
    "start_line": 5901,
    "end_line": 5905,
    "section": "Code spans"
  },
  {
    "markdown": "` `` `\n",
    "html": "<p><code>``</code></p>\n",
    "example": 330,
    "start_line": 5891,
    "end_line": 5895,
    "start_line": 5911,
    "end_line": 5915,
    "section": "Code spans"
  },
  {
    "markdown": "`  ``  `\n",
    "html": "<p><code> `` </code></p>\n",
    "example": 331,
    "start_line": 5899,
    "end_line": 5903,
    "start_line": 5919,
    "end_line": 5923,
    "section": "Code spans"
  },
  {
    "markdown": "` a`\n",
    "html": "<p><code> a</code></p>\n",
    "example": 332,
    "start_line": 5908,
    "end_line": 5912,
    "start_line": 5928,
    "end_line": 5932,
    "section": "Code spans"
  },
  {
    "markdown": "` b `\n",
    "html": "<p><code> b </code></p>\n",
    "example": 333,
    "start_line": 5917,
    "end_line": 5921,
    "start_line": 5937,
    "end_line": 5941,
    "section": "Code spans"
  },
  {
    "markdown": "` `\n`  `\n",
    "html": "<p><code> </code>\n<code>  </code></p>\n",
    "example": 334,
    "start_line": 5925,
    "end_line": 5931,
    "start_line": 5945,
    "end_line": 5951,
    "section": "Code spans"
  },
  {
    "markdown": "``\nfoo\nbar  \nbaz\n``\n",
    "html": "<p><code>foo bar   baz</code></p>\n",
    "example": 335,
    "start_line": 5936,
    "end_line": 5944,
    "start_line": 5956,
    "end_line": 5964,
    "section": "Code spans"
  },
  {
    "markdown": "``\nfoo \n``\n",
    "html": "<p><code>foo </code></p>\n",
    "example": 336,
    "start_line": 5946,
    "end_line": 5952,
    "start_line": 5966,
    "end_line": 5972,
    "section": "Code spans"
  },
  {
    "markdown": "`foo   bar \nbaz`\n",
    "html": "<p><code>foo   bar  baz</code></p>\n",
    "example": 337,
    "start_line": 5957,
    "end_line": 5962,
    "start_line": 5977,
    "end_line": 5982,
    "section": "Code spans"
  },
  {
    "markdown": "`foo\\`bar`\n",
    "html": "<p><code>foo\\</code>bar`</p>\n",
    "example": 338,
    "start_line": 5974,
    "end_line": 5978,
    "start_line": 5994,
    "end_line": 5998,
    "section": "Code spans"
  },
  {
    "markdown": "``foo`bar``\n",
    "html": "<p><code>foo`bar</code></p>\n",
    "example": 339,
    "start_line": 5985,
    "end_line": 5989,
    "start_line": 6005,
    "end_line": 6009,
    "section": "Code spans"
  },
  {
    "markdown": "` foo `` bar `\n",
    "html": "<p><code>foo `` bar</code></p>\n",
    "example": 340,
    "start_line": 5991,
    "end_line": 5995,
    "start_line": 6011,
    "end_line": 6015,
    "section": "Code spans"
  },
  {
    "markdown": "*foo`*`\n",
    "html": "<p>*foo<code>*</code></p>\n",
    "example": 341,
    "start_line": 6003,
    "end_line": 6007,
    "start_line": 6023,
    "end_line": 6027,
    "section": "Code spans"
  },
  {
    "markdown": "[not a `link](/foo`)\n",
    "html": "<p>[not a <code>link](/foo</code>)</p>\n",
    "example": 342,
    "start_line": 6012,
    "end_line": 6016,
    "start_line": 6032,
    "end_line": 6036,
    "section": "Code spans"
  },
  {
    "markdown": "`<a href=\"`\">`\n",
    "html": "<p><code>&lt;a href=&quot;</code>&quot;&gt;`</p>\n",
    "example": 343,
    "start_line": 6022,
    "end_line": 6026,
    "start_line": 6042,
    "end_line": 6046,
    "section": "Code spans"
  },
  {
    "markdown": "<a href=\"`\">`\n",
    "html": "<p><a href=\"`\">`</p>\n",
    "example": 344,
    "start_line": 6031,
    "end_line": 6035,
    "start_line": 6051,
    "end_line": 6055,
    "section": "Code spans"
  },
  {
    "markdown": "`<http://foo.bar.`baz>`\n",
    "html": "<p><code>&lt;http://foo.bar.</code>baz&gt;`</p>\n",
    "example": 345,
    "start_line": 6040,
    "end_line": 6044,
    "start_line": 6060,
    "end_line": 6064,
    "section": "Code spans"
  },
  {
    "markdown": "<http://foo.bar.`baz>`\n",
    "html": "<p><a href=\"http://foo.bar.%60baz\">http://foo.bar.`baz</a>`</p>\n",
    "example": 346,
    "start_line": 6049,
    "end_line": 6053,
    "start_line": 6069,
    "end_line": 6073,
    "section": "Code spans"
  },
  {
    "markdown": "```foo``\n",
    "html": "<p>```foo``</p>\n",
    "example": 347,
    "start_line": 6059,
    "end_line": 6063,
    "start_line": 6079,
    "end_line": 6083,
    "section": "Code spans"
  },
  {
    "markdown": "`foo\n",
    "html": "<p>`foo</p>\n",
    "example": 348,
    "start_line": 6066,
    "end_line": 6070,
    "start_line": 6086,
    "end_line": 6090,
    "section": "Code spans"
  },
  {
    "markdown": "`foo``bar``\n",
    "html": "<p>`foo<code>bar</code></p>\n",
    "example": 349,
    "start_line": 6075,
    "end_line": 6079,
    "start_line": 6095,
    "end_line": 6099,
    "section": "Code spans"
  },
  {
    "markdown": "*foo bar*\n",
    "html": "<p><em>foo bar</em></p>\n",
    "example": 350,
    "start_line": 6292,
    "end_line": 6296,
    "start_line": 6312,
    "end_line": 6316,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a * foo bar*\n",
    "html": "<p>a * foo bar*</p>\n",
    "example": 351,
    "start_line": 6302,
    "end_line": 6306,
    "start_line": 6322,
    "end_line": 6326,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a*\"foo\"*\n",
    "html": "<p>a*&quot;foo&quot;*</p>\n",
    "example": 352,
    "start_line": 6313,
    "end_line": 6317,
    "start_line": 6333,
    "end_line": 6337,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "* a *\n",
    "html": "<p>* a *</p>\n",
    "example": 353,
    "start_line": 6322,
    "end_line": 6326,
    "start_line": 6342,
    "end_line": 6346,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo*bar*\n",
    "html": "<p>foo<em>bar</em></p>\n",
    "example": 354,
    "start_line": 6331,
    "end_line": 6335,
    "start_line": 6351,
    "end_line": 6355,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "5*6*78\n",
    "html": "<p>5<em>6</em>78</p>\n",
    "example": 355,
    "start_line": 6338,
    "end_line": 6342,
    "start_line": 6358,
    "end_line": 6362,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo bar_\n",
    "html": "<p><em>foo bar</em></p>\n",
    "example": 356,
    "start_line": 6347,
    "end_line": 6351,
    "start_line": 6367,
    "end_line": 6371,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_ foo bar_\n",
    "html": "<p>_ foo bar_</p>\n",
    "example": 357,
    "start_line": 6357,
    "end_line": 6361,
    "start_line": 6377,
    "end_line": 6381,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a_\"foo\"_\n",
    "html": "<p>a_&quot;foo&quot;_</p>\n",
    "example": 358,
    "start_line": 6367,
    "end_line": 6371,
    "start_line": 6387,
    "end_line": 6391,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo_bar_\n",
    "html": "<p>foo_bar_</p>\n",
    "example": 359,
    "start_line": 6376,
    "end_line": 6380,
    "start_line": 6396,
    "end_line": 6400,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "5_6_78\n",
    "html": "<p>5_6_78</p>\n",
    "example": 360,
    "start_line": 6383,
    "end_line": 6387,
    "start_line": 6403,
    "end_line": 6407,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "пристаням_стремятся_\n",
    "html": "<p>пристаням_стремятся_</p>\n",
    "example": 361,
    "start_line": 6390,
    "end_line": 6394,
    "start_line": 6410,
    "end_line": 6414,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "aa_\"bb\"_cc\n",
    "html": "<p>aa_&quot;bb&quot;_cc</p>\n",
    "example": 362,
    "start_line": 6400,
    "end_line": 6404,
    "start_line": 6420,
    "end_line": 6424,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo-_(bar)_\n",
    "html": "<p>foo-<em>(bar)</em></p>\n",
    "example": 363,
    "start_line": 6411,
    "end_line": 6415,
    "start_line": 6431,
    "end_line": 6435,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo*\n",
    "html": "<p>_foo*</p>\n",
    "example": 364,
    "start_line": 6423,
    "end_line": 6427,
    "start_line": 6443,
    "end_line": 6447,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo bar *\n",
    "html": "<p>*foo bar *</p>\n",
    "example": 365,
    "start_line": 6433,
    "end_line": 6437,
    "start_line": 6453,
    "end_line": 6457,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo bar\n*\n",
    "html": "<p>*foo bar\n*</p>\n",
    "example": 366,
    "start_line": 6442,
    "end_line": 6448,
    "start_line": 6462,
    "end_line": 6468,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*(*foo)\n",
    "html": "<p>*(*foo)</p>\n",
    "example": 367,
    "start_line": 6455,
    "end_line": 6459,
    "start_line": 6475,
    "end_line": 6479,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*(*foo*)*\n",
    "html": "<p><em>(<em>foo</em>)</em></p>\n",
    "example": 368,
    "start_line": 6465,
    "end_line": 6469,
    "start_line": 6485,
    "end_line": 6489,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo*bar\n",
    "html": "<p><em>foo</em>bar</p>\n",
    "example": 369,
    "start_line": 6474,
    "end_line": 6478,
    "start_line": 6494,
    "end_line": 6498,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo bar _\n",
    "html": "<p>_foo bar _</p>\n",
    "example": 370,
    "start_line": 6487,
    "end_line": 6491,
    "start_line": 6507,
    "end_line": 6511,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(_foo)\n",
    "html": "<p>_(_foo)</p>\n",
    "example": 371,
    "start_line": 6497,
    "end_line": 6501,
    "start_line": 6517,
    "end_line": 6521,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(_foo_)_\n",
    "html": "<p><em>(<em>foo</em>)</em></p>\n",
    "example": 372,
    "start_line": 6506,
    "end_line": 6510,
    "start_line": 6526,
    "end_line": 6530,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo_bar\n",
    "html": "<p>_foo_bar</p>\n",
    "example": 373,
    "start_line": 6515,
    "end_line": 6519,
    "start_line": 6535,
    "end_line": 6539,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_пристаням_стремятся\n",
    "html": "<p>_пристаням_стремятся</p>\n",
    "example": 374,
    "start_line": 6522,
    "end_line": 6526,
    "start_line": 6542,
    "end_line": 6546,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo_bar_baz_\n",
    "html": "<p><em>foo_bar_baz</em></p>\n",
    "example": 375,
    "start_line": 6529,
    "end_line": 6533,
    "start_line": 6549,
    "end_line": 6553,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(bar)_.\n",
    "html": "<p><em>(bar)</em>.</p>\n",
    "example": 376,
    "start_line": 6540,
    "end_line": 6544,
    "start_line": 6560,
    "end_line": 6564,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo bar**\n",
    "html": "<p><strong>foo bar</strong></p>\n",
    "example": 377,
    "start_line": 6549,
    "end_line": 6553,
    "start_line": 6569,
    "end_line": 6573,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "** foo bar**\n",
    "html": "<p>** foo bar**</p>\n",
    "example": 378,
    "start_line": 6559,
    "end_line": 6563,
    "start_line": 6579,
    "end_line": 6583,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a**\"foo\"**\n",
    "html": "<p>a**&quot;foo&quot;**</p>\n",
    "example": 379,
    "start_line": 6570,
    "end_line": 6574,
    "start_line": 6590,
    "end_line": 6594,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo**bar**\n",
    "html": "<p>foo<strong>bar</strong></p>\n",
    "example": 380,
    "start_line": 6579,
    "end_line": 6583,
    "start_line": 6599,
    "end_line": 6603,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo bar__\n",
    "html": "<p><strong>foo bar</strong></p>\n",
    "example": 381,
    "start_line": 6588,
    "end_line": 6592,
    "start_line": 6608,
    "end_line": 6612,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__ foo bar__\n",
    "html": "<p>__ foo bar__</p>\n",
    "example": 382,
    "start_line": 6598,
    "end_line": 6602,
    "start_line": 6618,
    "end_line": 6622,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__\nfoo bar__\n",
    "html": "<p>__\nfoo bar__</p>\n",
    "example": 383,
    "start_line": 6606,
    "end_line": 6612,
    "start_line": 6626,
    "end_line": 6632,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a__\"foo\"__\n",
    "html": "<p>a__&quot;foo&quot;__</p>\n",
    "example": 384,
    "start_line": 6618,
    "end_line": 6622,
    "start_line": 6638,
    "end_line": 6642,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo__bar__\n",
    "html": "<p>foo__bar__</p>\n",
    "example": 385,
    "start_line": 6627,
    "end_line": 6631,
    "start_line": 6647,
    "end_line": 6651,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "5__6__78\n",
    "html": "<p>5__6__78</p>\n",
    "example": 386,
    "start_line": 6634,
    "end_line": 6638,
    "start_line": 6654,
    "end_line": 6658,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "пристаням__стремятся__\n",
    "html": "<p>пристаням__стремятся__</p>\n",
    "example": 387,
    "start_line": 6641,
    "end_line": 6645,
    "start_line": 6661,
    "end_line": 6665,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo, __bar__, baz__\n",
    "html": "<p><strong>foo, <strong>bar</strong>, baz</strong></p>\n",
    "example": 388,
    "start_line": 6648,
    "end_line": 6652,
    "start_line": 6668,
    "end_line": 6672,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo-__(bar)__\n",
    "html": "<p>foo-<strong>(bar)</strong></p>\n",
    "example": 389,
    "start_line": 6659,
    "end_line": 6663,
    "start_line": 6679,
    "end_line": 6683,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo bar **\n",
    "html": "<p>**foo bar **</p>\n",
    "example": 390,
    "start_line": 6672,
    "end_line": 6676,
    "start_line": 6692,
    "end_line": 6696,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**(**foo)\n",
    "html": "<p>**(**foo)</p>\n",
    "example": 391,
    "start_line": 6685,
    "end_line": 6689,
    "start_line": 6705,
    "end_line": 6709,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*(**foo**)*\n",
    "html": "<p><em>(<strong>foo</strong>)</em></p>\n",
    "example": 392,
    "start_line": 6695,
    "end_line": 6699,
    "start_line": 6715,
    "end_line": 6719,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n",
    "html": "<p><strong>Gomphocarpus (<em>Gomphocarpus physocarpus</em>, syn.\n<em>Asclepias physocarpa</em>)</strong></p>\n",
    "example": 393,
    "start_line": 6702,
    "end_line": 6708,
    "start_line": 6722,
    "end_line": 6728,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo \"*bar*\" foo**\n",
    "html": "<p><strong>foo &quot;<em>bar</em>&quot; foo</strong></p>\n",
    "example": 394,
    "start_line": 6711,
    "end_line": 6715,
    "start_line": 6731,
    "end_line": 6735,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo**bar\n",
    "html": "<p><strong>foo</strong>bar</p>\n",
    "example": 395,
    "start_line": 6720,
    "end_line": 6724,
    "start_line": 6740,
    "end_line": 6744,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo bar __\n",
    "html": "<p>__foo bar __</p>\n",
    "example": 396,
    "start_line": 6732,
    "end_line": 6736,
    "start_line": 6752,
    "end_line": 6756,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__(__foo)\n",
    "html": "<p>__(__foo)</p>\n",
    "example": 397,
    "start_line": 6742,
    "end_line": 6746,
    "start_line": 6762,
    "end_line": 6766,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(__foo__)_\n",
    "html": "<p><em>(<strong>foo</strong>)</em></p>\n",
    "example": 398,
    "start_line": 6752,
    "end_line": 6756,
    "start_line": 6772,
    "end_line": 6776,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo__bar\n",
    "html": "<p>__foo__bar</p>\n",
    "example": 399,
    "start_line": 6761,
    "end_line": 6765,
    "start_line": 6781,
    "end_line": 6785,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__пристаням__стремятся\n",
    "html": "<p>__пристаням__стремятся</p>\n",
    "example": 400,
    "start_line": 6768,
    "end_line": 6772,
    "start_line": 6788,
    "end_line": 6792,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo__bar__baz__\n",
    "html": "<p><strong>foo__bar__baz</strong></p>\n",
    "example": 401,
    "start_line": 6775,
    "end_line": 6779,
    "start_line": 6795,
    "end_line": 6799,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__(bar)__.\n",
    "html": "<p><strong>(bar)</strong>.</p>\n",
    "example": 402,
    "start_line": 6786,
    "end_line": 6790,
    "start_line": 6806,
    "end_line": 6810,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo [bar](/url)*\n",
    "html": "<p><em>foo <a href=\"/url\">bar</a></em></p>\n",
    "example": 403,
    "start_line": 6798,
    "end_line": 6802,
    "start_line": 6818,
    "end_line": 6822,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo\nbar*\n",
    "html": "<p><em>foo\nbar</em></p>\n",
    "example": 404,
    "start_line": 6805,
    "end_line": 6811,
    "start_line": 6825,
    "end_line": 6831,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo __bar__ baz_\n",
    "html": "<p><em>foo <strong>bar</strong> baz</em></p>\n",
    "example": 405,
    "start_line": 6817,
    "end_line": 6821,
    "start_line": 6837,
    "end_line": 6841,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo _bar_ baz_\n",
    "html": "<p><em>foo <em>bar</em> baz</em></p>\n",
    "example": 406,
    "start_line": 6824,
    "end_line": 6828,
    "start_line": 6844,
    "end_line": 6848,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo_ bar_\n",
    "html": "<p><em><em>foo</em> bar</em></p>\n",
    "example": 407,
    "start_line": 6831,
    "end_line": 6835,
    "start_line": 6851,
    "end_line": 6855,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo *bar**\n",
    "html": "<p><em>foo <em>bar</em></em></p>\n",
    "example": 408,
    "start_line": 6838,
    "end_line": 6842,
    "start_line": 6858,
    "end_line": 6862,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo **bar** baz*\n",
    "html": "<p><em>foo <strong>bar</strong> baz</em></p>\n",
    "example": 409,
    "start_line": 6845,
    "end_line": 6849,
    "start_line": 6865,
    "end_line": 6869,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo**bar**baz*\n",
    "html": "<p><em>foo<strong>bar</strong>baz</em></p>\n",
    "example": 410,
    "start_line": 6851,
    "end_line": 6855,
    "start_line": 6871,
    "end_line": 6875,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo**bar*\n",
    "html": "<p><em>foo**bar</em></p>\n",
    "example": 411,
    "start_line": 6875,
    "end_line": 6879,
    "start_line": 6895,
    "end_line": 6899,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo** bar*\n",
    "html": "<p><em><strong>foo</strong> bar</em></p>\n",
    "example": 412,
    "start_line": 6888,
    "end_line": 6892,
    "start_line": 6908,
    "end_line": 6912,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo **bar***\n",
    "html": "<p><em>foo <strong>bar</strong></em></p>\n",
    "example": 413,
    "start_line": 6895,
    "end_line": 6899,
    "start_line": 6915,
    "end_line": 6919,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo**bar***\n",
    "html": "<p><em>foo<strong>bar</strong></em></p>\n",
    "example": 414,
    "start_line": 6902,
    "end_line": 6906,
    "start_line": 6922,
    "end_line": 6926,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo***bar***baz\n",
    "html": "<p>foo<em><strong>bar</strong></em>baz</p>\n",
    "example": 415,
    "start_line": 6913,
    "end_line": 6917,
    "start_line": 6933,
    "end_line": 6937,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo******bar*********baz\n",
    "html": "<p>foo<strong><strong><strong>bar</strong></strong></strong>***baz</p>\n",
    "example": 416,
    "start_line": 6919,
    "end_line": 6923,
    "start_line": 6939,
    "end_line": 6943,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo **bar *baz* bim** bop*\n",
    "html": "<p><em>foo <strong>bar <em>baz</em> bim</strong> bop</em></p>\n",
    "example": 417,
    "start_line": 6928,
    "end_line": 6932,
    "start_line": 6948,
    "end_line": 6952,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo [*bar*](/url)*\n",
    "html": "<p><em>foo <a href=\"/url\"><em>bar</em></a></em></p>\n",
    "example": 418,
    "start_line": 6935,
    "end_line": 6939,
    "start_line": 6955,
    "end_line": 6959,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "** is not an empty emphasis\n",
    "html": "<p>** is not an empty emphasis</p>\n",
    "example": 419,
    "start_line": 6944,
    "end_line": 6948,
    "start_line": 6964,
    "end_line": 6968,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**** is not an empty strong emphasis\n",
    "html": "<p>**** is not an empty strong emphasis</p>\n",
    "example": 420,
    "start_line": 6951,
    "end_line": 6955,
    "start_line": 6971,
    "end_line": 6975,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo [bar](/url)**\n",
    "html": "<p><strong>foo <a href=\"/url\">bar</a></strong></p>\n",
    "example": 421,
    "start_line": 6964,
    "end_line": 6968,
    "start_line": 6984,
    "end_line": 6988,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo\nbar**\n",
    "html": "<p><strong>foo\nbar</strong></p>\n",
    "example": 422,
    "start_line": 6971,
    "end_line": 6977,
    "start_line": 6991,
    "end_line": 6997,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo _bar_ baz__\n",
    "html": "<p><strong>foo <em>bar</em> baz</strong></p>\n",
    "example": 423,
    "start_line": 6983,
    "end_line": 6987,
    "start_line": 7003,
    "end_line": 7007,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo __bar__ baz__\n",
    "html": "<p><strong>foo <strong>bar</strong> baz</strong></p>\n",
    "example": 424,
    "start_line": 6990,
    "end_line": 6994,
    "start_line": 7010,
    "end_line": 7014,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "____foo__ bar__\n",
    "html": "<p><strong><strong>foo</strong> bar</strong></p>\n",
    "example": 425,
    "start_line": 6997,
    "end_line": 7001,
    "start_line": 7017,
    "end_line": 7021,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo **bar****\n",
    "html": "<p><strong>foo <strong>bar</strong></strong></p>\n",
    "example": 426,
    "start_line": 7004,
    "end_line": 7008,
    "start_line": 7024,
    "end_line": 7028,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo *bar* baz**\n",
    "html": "<p><strong>foo <em>bar</em> baz</strong></p>\n",
    "example": 427,
    "start_line": 7011,
    "end_line": 7015,
    "start_line": 7031,
    "end_line": 7035,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo*bar*baz**\n",
    "html": "<p><strong>foo<em>bar</em>baz</strong></p>\n",
    "example": 428,
    "start_line": 7018,
    "end_line": 7022,
    "start_line": 7038,
    "end_line": 7042,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo* bar**\n",
    "html": "<p><strong><em>foo</em> bar</strong></p>\n",
    "example": 429,
    "start_line": 7025,
    "end_line": 7029,
    "start_line": 7045,
    "end_line": 7049,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo *bar***\n",
    "html": "<p><strong>foo <em>bar</em></strong></p>\n",
    "example": 430,
    "start_line": 7032,
    "end_line": 7036,
    "start_line": 7052,
    "end_line": 7056,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo *bar **baz**\nbim* bop**\n",
    "html": "<p><strong>foo <em>bar <strong>baz</strong>\nbim</em> bop</strong></p>\n",
    "example": 431,
    "start_line": 7041,
    "end_line": 7047,
    "start_line": 7061,
    "end_line": 7067,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo [*bar*](/url)**\n",
    "html": "<p><strong>foo <a href=\"/url\"><em>bar</em></a></strong></p>\n",
    "example": 432,
    "start_line": 7050,
    "end_line": 7054,
    "start_line": 7070,
    "end_line": 7074,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__ is not an empty emphasis\n",
    "html": "<p>__ is not an empty emphasis</p>\n",
    "example": 433,
    "start_line": 7059,
    "end_line": 7063,
    "start_line": 7079,
    "end_line": 7083,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "____ is not an empty strong emphasis\n",
    "html": "<p>____ is not an empty strong emphasis</p>\n",
    "example": 434,
    "start_line": 7066,
    "end_line": 7070,
    "start_line": 7086,
    "end_line": 7090,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo ***\n",
    "html": "<p>foo ***</p>\n",
    "example": 435,
    "start_line": 7076,
    "end_line": 7080,
    "start_line": 7096,
    "end_line": 7100,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo *\\**\n",
    "html": "<p>foo <em>*</em></p>\n",
    "example": 436,
    "start_line": 7083,
    "end_line": 7087,
    "start_line": 7103,
    "end_line": 7107,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo *_*\n",
    "html": "<p>foo <em>_</em></p>\n",
    "example": 437,
    "start_line": 7090,
    "end_line": 7094,
    "start_line": 7110,
    "end_line": 7114,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo *****\n",
    "html": "<p>foo *****</p>\n",
    "example": 438,
    "start_line": 7097,
    "end_line": 7101,
    "start_line": 7117,
    "end_line": 7121,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo **\\***\n",
    "html": "<p>foo <strong>*</strong></p>\n",
    "example": 439,
    "start_line": 7104,
    "end_line": 7108,
    "start_line": 7124,
    "end_line": 7128,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo **_**\n",
    "html": "<p>foo <strong>_</strong></p>\n",
    "example": 440,
    "start_line": 7111,
    "end_line": 7115,
    "start_line": 7131,
    "end_line": 7135,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo*\n",
    "html": "<p>*<em>foo</em></p>\n",
    "example": 441,
    "start_line": 7122,
    "end_line": 7126,
    "start_line": 7142,
    "end_line": 7146,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo**\n",
    "html": "<p><em>foo</em>*</p>\n",
    "example": 442,
    "start_line": 7129,
    "end_line": 7133,
    "start_line": 7149,
    "end_line": 7153,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo**\n",
    "html": "<p>*<strong>foo</strong></p>\n",
    "example": 443,
    "start_line": 7136,
    "end_line": 7140,
    "start_line": 7156,
    "end_line": 7160,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "****foo*\n",
    "html": "<p>***<em>foo</em></p>\n",
    "example": 444,
    "start_line": 7143,
    "end_line": 7147,
    "start_line": 7163,
    "end_line": 7167,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo***\n",
    "html": "<p><strong>foo</strong>*</p>\n",
    "example": 445,
    "start_line": 7150,
    "end_line": 7154,
    "start_line": 7170,
    "end_line": 7174,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo****\n",
    "html": "<p><em>foo</em>***</p>\n",
    "example": 446,
    "start_line": 7157,
    "end_line": 7161,
    "start_line": 7177,
    "end_line": 7181,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo ___\n",
    "html": "<p>foo ___</p>\n",
    "example": 447,
    "start_line": 7167,
    "end_line": 7171,
    "start_line": 7187,
    "end_line": 7191,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo _\\__\n",
    "html": "<p>foo <em>_</em></p>\n",
    "example": 448,
    "start_line": 7174,
    "end_line": 7178,
    "start_line": 7194,
    "end_line": 7198,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo _*_\n",
    "html": "<p>foo <em>*</em></p>\n",
    "example": 449,
    "start_line": 7181,
    "end_line": 7185,
    "start_line": 7201,
    "end_line": 7205,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo _____\n",
    "html": "<p>foo _____</p>\n",
    "example": 450,
    "start_line": 7188,
    "end_line": 7192,
    "start_line": 7208,
    "end_line": 7212,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo __\\___\n",
    "html": "<p>foo <strong>_</strong></p>\n",
    "example": 451,
    "start_line": 7195,
    "end_line": 7199,
    "start_line": 7215,
    "end_line": 7219,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo __*__\n",
    "html": "<p>foo <strong>*</strong></p>\n",
    "example": 452,
    "start_line": 7202,
    "end_line": 7206,
    "start_line": 7222,
    "end_line": 7226,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo_\n",
    "html": "<p>_<em>foo</em></p>\n",
    "example": 453,
    "start_line": 7209,
    "end_line": 7213,
    "start_line": 7229,
    "end_line": 7233,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo__\n",
    "html": "<p><em>foo</em>_</p>\n",
    "example": 454,
    "start_line": 7220,
    "end_line": 7224,
    "start_line": 7240,
    "end_line": 7244,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "___foo__\n",
    "html": "<p>_<strong>foo</strong></p>\n",
    "example": 455,
    "start_line": 7227,
    "end_line": 7231,
    "start_line": 7247,
    "end_line": 7251,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "____foo_\n",
    "html": "<p>___<em>foo</em></p>\n",
    "example": 456,
    "start_line": 7234,
    "end_line": 7238,
    "start_line": 7254,
    "end_line": 7258,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo___\n",
    "html": "<p><strong>foo</strong>_</p>\n",
    "example": 457,
    "start_line": 7241,
    "end_line": 7245,
    "start_line": 7261,
    "end_line": 7265,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo____\n",
    "html": "<p><em>foo</em>___</p>\n",
    "example": 458,
    "start_line": 7248,
    "end_line": 7252,
    "start_line": 7268,
    "end_line": 7272,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo**\n",
    "html": "<p><strong>foo</strong></p>\n",
    "example": 459,
    "start_line": 7258,
    "end_line": 7262,
    "start_line": 7278,
    "end_line": 7282,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*_foo_*\n",
    "html": "<p><em><em>foo</em></em></p>\n",
    "example": 460,
    "start_line": 7265,
    "end_line": 7269,
    "start_line": 7285,
    "end_line": 7289,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo__\n",
    "html": "<p><strong>foo</strong></p>\n",
    "example": 461,
    "start_line": 7272,
    "end_line": 7276,
    "start_line": 7292,
    "end_line": 7296,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_*foo*_\n",
    "html": "<p><em><em>foo</em></em></p>\n",
    "example": 462,
    "start_line": 7279,
    "end_line": 7283,
    "start_line": 7299,
    "end_line": 7303,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "****foo****\n",
    "html": "<p><strong><strong>foo</strong></strong></p>\n",
    "example": 463,
    "start_line": 7289,
    "end_line": 7293,
    "start_line": 7309,
    "end_line": 7313,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "____foo____\n",
    "html": "<p><strong><strong>foo</strong></strong></p>\n",
    "example": 464,
    "start_line": 7296,
    "end_line": 7300,
    "start_line": 7316,
    "end_line": 7320,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "******foo******\n",
    "html": "<p><strong><strong><strong>foo</strong></strong></strong></p>\n",
    "example": 465,
    "start_line": 7307,
    "end_line": 7311,
    "start_line": 7327,
    "end_line": 7331,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo***\n",
    "html": "<p><em><strong>foo</strong></em></p>\n",
    "example": 466,
    "start_line": 7316,
    "end_line": 7320,
    "start_line": 7336,
    "end_line": 7340,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_____foo_____\n",
    "html": "<p><em><strong><strong>foo</strong></strong></em></p>\n",
    "example": 467,
    "start_line": 7323,
    "end_line": 7327,
    "start_line": 7343,
    "end_line": 7347,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo _bar* baz_\n",
    "html": "<p><em>foo _bar</em> baz_</p>\n",
    "example": 468,
    "start_line": 7332,
    "end_line": 7336,
    "start_line": 7352,
    "end_line": 7356,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo __bar *baz bim__ bam*\n",
    "html": "<p><em>foo <strong>bar *baz bim</strong> bam</em></p>\n",
    "example": 469,
    "start_line": 7339,
    "end_line": 7343,
    "start_line": 7359,
    "end_line": 7363,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo **bar baz**\n",
    "html": "<p>**foo <strong>bar baz</strong></p>\n",
    "example": 470,
    "start_line": 7348,
    "end_line": 7352,
    "start_line": 7368,
    "end_line": 7372,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo *bar baz*\n",
    "html": "<p>*foo <em>bar baz</em></p>\n",
    "example": 471,
    "start_line": 7355,
    "end_line": 7359,
    "start_line": 7375,
    "end_line": 7379,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*[bar*](/url)\n",
    "html": "<p>*<a href=\"/url\">bar*</a></p>\n",
    "example": 472,
    "start_line": 7364,
    "end_line": 7368,
    "start_line": 7384,
    "end_line": 7388,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo [bar_](/url)\n",
    "html": "<p>_foo <a href=\"/url\">bar_</a></p>\n",
    "example": 473,
    "start_line": 7371,
    "end_line": 7375,
    "start_line": 7391,
    "end_line": 7395,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*<img src=\"foo\" title=\"*\"/>\n",
    "html": "<p>*<img src=\"foo\" title=\"*\"/></p>\n",
    "example": 474,
    "start_line": 7378,
    "end_line": 7382,
    "start_line": 7398,
    "end_line": 7402,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**<a href=\"**\">\n",
    "html": "<p>**<a href=\"**\"></p>\n",
    "example": 475,
    "start_line": 7385,
    "end_line": 7389,
    "start_line": 7405,
    "end_line": 7409,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__<a href=\"__\">\n",
    "html": "<p>__<a href=\"__\"></p>\n",
    "example": 476,
    "start_line": 7392,
    "end_line": 7396,
    "start_line": 7412,
    "end_line": 7416,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*a `*`*\n",
    "html": "<p><em>a <code>*</code></em></p>\n",
    "example": 477,
    "start_line": 7399,
    "end_line": 7403,
    "start_line": 7419,
    "end_line": 7423,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_a `_`_\n",
    "html": "<p><em>a <code>_</code></em></p>\n",
    "example": 478,
    "start_line": 7406,
    "end_line": 7410,
    "start_line": 7426,
    "end_line": 7430,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**a<http://foo.bar/?q=**>\n",
    "html": "<p>**a<a href=\"http://foo.bar/?q=**\">http://foo.bar/?q=**</a></p>\n",
    "example": 479,
    "start_line": 7413,
    "end_line": 7417,
    "start_line": 7433,
    "end_line": 7437,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__a<http://foo.bar/?q=__>\n",
    "html": "<p>__a<a href=\"http://foo.bar/?q=__\">http://foo.bar/?q=__</a></p>\n",
    "example": 480,
    "start_line": 7420,
    "end_line": 7424,
    "start_line": 7440,
    "end_line": 7444,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "[link](/uri \"title\")\n",
    "html": "<p><a href=\"/uri\" title=\"title\">link</a></p>\n",
    "example": 481,
    "start_line": 7503,
    "end_line": 7507,
    "start_line": 7528,
    "end_line": 7532,
    "section": "Links"
  },
  {
    "markdown": "[link](/uri)\n",
    "html": "<p><a href=\"/uri\">link</a></p>\n",
    "example": 482,
    "start_line": 7512,
    "end_line": 7516,
    "start_line": 7538,
    "end_line": 7542,
    "section": "Links"
  },
  {
    "markdown": "[](./target.md)\n",
    "html": "<p><a href=\"./target.md\"></a></p>\n",
    "example": 483,
    "start_line": 7544,
    "end_line": 7548,
    "section": "Links"
  },
  {
    "markdown": "[link]()\n",
    "html": "<p><a href=\"\">link</a></p>\n",
    "example": 483,
    "start_line": 7521,
    "end_line": 7525,
    "example": 484,
    "start_line": 7551,
    "end_line": 7555,
    "section": "Links"
  },
  {
    "markdown": "[link](<>)\n",
    "html": "<p><a href=\"\">link</a></p>\n",
    "example": 484,
    "start_line": 7528,
    "end_line": 7532,
    "example": 485,
    "start_line": 7558,
    "end_line": 7562,
    "section": "Links"
  },
  {
    "markdown": "[]()\n",
    "html": "<p><a href=\"\"></a></p>\n",
    "example": 486,
    "start_line": 7565,
    "end_line": 7569,
    "section": "Links"
  },
  {
    "markdown": "[link](/my uri)\n",
    "html": "<p>[link](/my uri)</p>\n",
    "example": 485,
    "start_line": 7537,
    "end_line": 7541,
    "example": 487,
    "start_line": 7574,
    "end_line": 7578,
    "section": "Links"
  },
  {
    "markdown": "[link](</my uri>)\n",
    "html": "<p><a href=\"/my%20uri\">link</a></p>\n",
    "example": 486,
    "start_line": 7543,
    "end_line": 7547,
    "example": 488,
    "start_line": 7580,
    "end_line": 7584,
    "section": "Links"
  },
  {
    "markdown": "[link](foo\nbar)\n",
    "html": "<p>[link](foo\nbar)</p>\n",
    "example": 487,
    "start_line": 7552,
    "end_line": 7558,
    "example": 489,
    "start_line": 7589,
    "end_line": 7595,
    "section": "Links"
  },
  {
    "markdown": "[link](<foo\nbar>)\n",
    "html": "<p>[link](<foo\nbar>)</p>\n",
    "example": 488,
    "start_line": 7560,
    "end_line": 7566,
    "example": 490,
    "start_line": 7597,
    "end_line": 7603,
    "section": "Links"
  },
  {
    "markdown": "[a](<b)c>)\n",
    "html": "<p><a href=\"b)c\">a</a></p>\n",
    "example": 489,
    "start_line": 7571,
    "end_line": 7575,
    "example": 491,
    "start_line": 7608,
    "end_line": 7612,
    "section": "Links"
  },
  {
    "markdown": "[link](<foo\\>)\n",
    "html": "<p>[link](&lt;foo&gt;)</p>\n",
    "example": 490,
    "start_line": 7579,
    "end_line": 7583,
    "example": 492,
    "start_line": 7616,
    "end_line": 7620,
    "section": "Links"
  },
  {
    "markdown": "[a](<b)c\n[a](<b)c>\n[a](<b>c)\n",
    "html": "<p>[a](&lt;b)c\n[a](&lt;b)c&gt;\n[a](<b>c)</p>\n",
    "example": 491,
    "start_line": 7588,
    "end_line": 7596,
    "example": 493,
    "start_line": 7625,
    "end_line": 7633,
    "section": "Links"
  },
  {
    "markdown": "[link](\\(foo\\))\n",
    "html": "<p><a href=\"(foo)\">link</a></p>\n",
    "example": 492,
    "start_line": 7600,
    "end_line": 7604,
    "example": 494,
    "start_line": 7637,
    "end_line": 7641,
    "section": "Links"
  },
  {
    "markdown": "[link](foo(and(bar)))\n",
    "html": "<p><a href=\"foo(and(bar))\">link</a></p>\n",
    "example": 493,
    "start_line": 7609,
    "end_line": 7613,
    "example": 495,
    "start_line": 7646,
    "end_line": 7650,
    "section": "Links"
  },
  {
    "markdown": "[link](foo(and(bar))\n",
    "html": "<p>[link](foo(and(bar))</p>\n",
    "example": 496,
    "start_line": 7655,
    "end_line": 7659,
    "section": "Links"
  },
  {
    "markdown": "[link](foo\\(and\\(bar\\))\n",
    "html": "<p><a href=\"foo(and(bar)\">link</a></p>\n",
    "example": 494,
    "start_line": 7618,
    "end_line": 7622,
    "example": 497,
    "start_line": 7662,
    "end_line": 7666,
    "section": "Links"
  },
  {
    "markdown": "[link](<foo(and(bar)>)\n",
    "html": "<p><a href=\"foo(and(bar)\">link</a></p>\n",
    "example": 495,
    "start_line": 7625,
    "end_line": 7629,
    "example": 498,
    "start_line": 7669,
    "end_line": 7673,
    "section": "Links"
  },
  {
    "markdown": "[link](foo\\)\\:)\n",
    "html": "<p><a href=\"foo):\">link</a></p>\n",
    "example": 496,
    "start_line": 7635,
    "end_line": 7639,
    "example": 499,
    "start_line": 7679,
    "end_line": 7683,
    "section": "Links"
  },
  {
    "markdown": "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n",
    "html": "<p><a href=\"#fragment\">link</a></p>\n<p><a href=\"http://example.com#fragment\">link</a></p>\n<p><a href=\"http://example.com?foo=3#frag\">link</a></p>\n",
    "example": 497,
    "start_line": 7644,
    "end_line": 7654,
    "example": 500,
    "start_line": 7688,
    "end_line": 7698,
    "section": "Links"
  },
  {
    "markdown": "[link](foo\\bar)\n",
    "html": "<p><a href=\"foo%5Cbar\">link</a></p>\n",
    "example": 498,
    "start_line": 7660,
    "end_line": 7664,
    "example": 501,
    "start_line": 7704,
    "end_line": 7708,
    "section": "Links"
  },
  {
    "markdown": "[link](foo%20b&auml;)\n",
    "html": "<p><a href=\"foo%20b%C3%A4\">link</a></p>\n",
    "example": 499,
    "start_line": 7676,
    "end_line": 7680,
    "example": 502,
    "start_line": 7720,
    "end_line": 7724,
    "section": "Links"
  },
  {
    "markdown": "[link](\"title\")\n",
    "html": "<p><a href=\"%22title%22\">link</a></p>\n",
    "example": 500,
    "start_line": 7687,
    "end_line": 7691,
    "example": 503,
    "start_line": 7731,
    "end_line": 7735,
    "section": "Links"
  },
  {
    "markdown": "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n",
    "html": "<p><a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a></p>\n",
    "example": 501,
    "start_line": 7696,
    "end_line": 7704,
    "example": 504,
    "start_line": 7740,
    "end_line": 7748,
    "section": "Links"
  },
  {
    "markdown": "[link](/url \"title \\\"&quot;\")\n",
    "html": "<p><a href=\"/url\" title=\"title &quot;&quot;\">link</a></p>\n",
    "example": 502,
    "start_line": 7710,
    "end_line": 7714,
    "example": 505,
    "start_line": 7754,
    "end_line": 7758,
    "section": "Links"
  },
  {
    "markdown": "[link](/url \"title\")\n",
    "html": "<p><a href=\"/url%C2%A0%22title%22\">link</a></p>\n",
    "example": 503,
    "start_line": 7720,
    "end_line": 7724,
    "example": 506,
    "start_line": 7765,
    "end_line": 7769,
    "section": "Links"
  },
  {
    "markdown": "[link](/url \"title \"and\" title\")\n",
    "html": "<p>[link](/url &quot;title &quot;and&quot; title&quot;)</p>\n",
    "example": 504,
    "start_line": 7729,
    "end_line": 7733,
    "example": 507,
    "start_line": 7774,
    "end_line": 7778,
    "section": "Links"
  },
  {
    "markdown": "[link](/url 'title \"and\" title')\n",
    "html": "<p><a href=\"/url\" title=\"title &quot;and&quot; title\">link</a></p>\n",
    "example": 505,
    "start_line": 7738,
    "end_line": 7742,
    "example": 508,
    "start_line": 7783,
    "end_line": 7787,
    "section": "Links"
  },
  {
    "markdown": "[link](   /uri\n  \"title\"  )\n",
    "html": "<p><a href=\"/uri\" title=\"title\">link</a></p>\n",
    "example": 506,
    "start_line": 7762,
    "end_line": 7767,
    "example": 509,
    "start_line": 7808,
    "end_line": 7813,
    "section": "Links"
  },
  {
    "markdown": "[link] (/uri)\n",
    "html": "<p>[link] (/uri)</p>\n",
    "example": 507,
    "start_line": 7773,
    "end_line": 7777,
    "example": 510,
    "start_line": 7819,
    "end_line": 7823,
    "section": "Links"
  },
  {
    "markdown": "[link [foo [bar]]](/uri)\n",
    "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n",
    "example": 508,
    "start_line": 7783,
    "end_line": 7787,
    "example": 511,
    "start_line": 7829,
    "end_line": 7833,
    "section": "Links"
  },
  {
    "markdown": "[link] bar](/uri)\n",
    "html": "<p>[link] bar](/uri)</p>\n",
    "example": 509,
    "start_line": 7790,
    "end_line": 7794,
    "example": 512,
    "start_line": 7836,
    "end_line": 7840,
    "section": "Links"
  },
  {
    "markdown": "[link [bar](/uri)\n",
    "html": "<p>[link <a href=\"/uri\">bar</a></p>\n",
    "example": 510,
    "start_line": 7797,
    "end_line": 7801,
    "example": 513,
    "start_line": 7843,
    "end_line": 7847,
    "section": "Links"
  },
  {
    "markdown": "[link \\[bar](/uri)\n",
    "html": "<p><a href=\"/uri\">link [bar</a></p>\n",
    "example": 511,
    "start_line": 7804,
    "end_line": 7808,
    "example": 514,
    "start_line": 7850,
    "end_line": 7854,
    "section": "Links"
  },
  {
    "markdown": "[link *foo **bar** `#`*](/uri)\n",
    "html": "<p><a href=\"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\n",
    "example": 512,
    "start_line": 7813,
    "end_line": 7817,
    "example": 515,
    "start_line": 7859,
    "end_line": 7863,
    "section": "Links"
  },
  {
    "markdown": "[![moon](moon.jpg)](/uri)\n",
    "html": "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\" /></a></p>\n",
    "example": 513,
    "start_line": 7820,
    "end_line": 7824,
    "example": 516,
    "start_line": 7866,
    "end_line": 7870,
    "section": "Links"
  },
  {
    "markdown": "[foo [bar](/uri)](/uri)\n",
    "html": "<p>[foo <a href=\"/uri\">bar</a>](/uri)</p>\n",
    "example": 514,
    "start_line": 7829,
    "end_line": 7833,
    "example": 517,
    "start_line": 7875,
    "end_line": 7879,
    "section": "Links"
  },
  {
    "markdown": "[foo *[bar [baz](/uri)](/uri)*](/uri)\n",
    "html": "<p>[foo <em>[bar <a href=\"/uri\">baz</a>](/uri)</em>](/uri)</p>\n",
    "example": 515,
    "start_line": 7836,
    "end_line": 7840,
    "example": 518,
    "start_line": 7882,
    "end_line": 7886,
    "section": "Links"
  },
  {
    "markdown": "![[[foo](uri1)](uri2)](uri3)\n",
    "html": "<p><img src=\"uri3\" alt=\"[foo](uri2)\" /></p>\n",
    "example": 516,
    "start_line": 7843,
    "end_line": 7847,
    "example": 519,
    "start_line": 7889,
    "end_line": 7893,
    "section": "Links"
  },
  {
    "markdown": "*[foo*](/uri)\n",
    "html": "<p>*<a href=\"/uri\">foo*</a></p>\n",
    "example": 517,
    "start_line": 7853,
    "end_line": 7857,
    "example": 520,
    "start_line": 7899,
    "end_line": 7903,
    "section": "Links"
  },
  {
    "markdown": "[foo *bar](baz*)\n",
    "html": "<p><a href=\"baz*\">foo *bar</a></p>\n",
    "example": 518,
    "start_line": 7860,
    "end_line": 7864,
    "example": 521,
    "start_line": 7906,
    "end_line": 7910,
    "section": "Links"
  },
  {
    "markdown": "*foo [bar* baz]\n",
    "html": "<p><em>foo [bar</em> baz]</p>\n",
    "example": 519,
    "start_line": 7870,
    "end_line": 7874,
    "example": 522,
    "start_line": 7916,
    "end_line": 7920,
    "section": "Links"
  },
  {
    "markdown": "[foo <bar attr=\"](baz)\">\n",
    "html": "<p>[foo <bar attr=\"](baz)\"></p>\n",
    "example": 520,
    "start_line": 7880,
    "end_line": 7884,
    "example": 523,
    "start_line": 7926,
    "end_line": 7930,
    "section": "Links"
  },
  {
    "markdown": "[foo`](/uri)`\n",
    "html": "<p>[foo<code>](/uri)</code></p>\n",
    "example": 521,
    "start_line": 7887,
    "end_line": 7891,
    "example": 524,
    "start_line": 7933,
    "end_line": 7937,
    "section": "Links"
  },
  {
    "markdown": "[foo<http://example.com/?search=](uri)>\n",
    "html": "<p>[foo<a href=\"http://example.com/?search=%5D(uri)\">http://example.com/?search=](uri)</a></p>\n",
    "example": 522,
    "start_line": 7894,
    "end_line": 7898,
    "example": 525,
    "start_line": 7940,
    "end_line": 7944,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 523,
    "start_line": 7932,
    "end_line": 7938,
    "example": 526,
    "start_line": 7978,
    "end_line": 7984,
    "section": "Links"
  },
  {
    "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n",
    "example": 524,
    "start_line": 7947,
    "end_line": 7953,
    "example": 527,
    "start_line": 7993,
    "end_line": 7999,
    "section": "Links"
  },
  {
    "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\">link [bar</a></p>\n",
    "example": 525,
    "start_line": 7956,
    "end_line": 7962,
    "example": 528,
    "start_line": 8002,
    "end_line": 8008,
    "section": "Links"
  },
  {
    "markdown": "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\n",
    "example": 526,
    "start_line": 7967,
    "end_line": 7973,
    "example": 529,
    "start_line": 8013,
    "end_line": 8019,
    "section": "Links"
  },
  {
    "markdown": "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\" /></a></p>\n",
    "example": 527,
    "start_line": 7976,
    "end_line": 7982,
    "example": 530,
    "start_line": 8022,
    "end_line": 8028,
    "section": "Links"
  },
  {
    "markdown": "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n",
    "html": "<p>[foo <a href=\"/uri\">bar</a>]<a href=\"/uri\">ref</a></p>\n",
    "example": 528,
    "start_line": 7987,
    "end_line": 7993,
    "example": 531,
    "start_line": 8033,
    "end_line": 8039,
    "section": "Links"
  },
  {
    "markdown": "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n",
    "html": "<p>[foo <em>bar <a href=\"/uri\">baz</a></em>]<a href=\"/uri\">ref</a></p>\n",
    "example": 529,
    "start_line": 7996,
    "end_line": 8002,
    "example": 532,
    "start_line": 8042,
    "end_line": 8048,
    "section": "Links"
  },
  {
    "markdown": "*[foo*][ref]\n\n[ref]: /uri\n",
    "html": "<p>*<a href=\"/uri\">foo*</a></p>\n",
    "example": 530,
    "start_line": 8011,
    "end_line": 8017,
    "example": 533,
    "start_line": 8057,
    "end_line": 8063,
    "section": "Links"
  },
  {
    "markdown": "[foo *bar][ref]\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\">foo *bar</a></p>\n",
    "example": 531,
    "start_line": 8020,
    "end_line": 8026,
    "markdown": "[foo *bar][ref]*\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\">foo *bar</a>*</p>\n",
    "example": 534,
    "start_line": 8066,
    "end_line": 8072,
    "section": "Links"
  },
  {
    "markdown": "[foo <bar attr=\"][ref]\">\n\n[ref]: /uri\n",
    "html": "<p>[foo <bar attr=\"][ref]\"></p>\n",
    "example": 532,
    "start_line": 8032,
    "end_line": 8038,
    "example": 535,
    "start_line": 8078,
    "end_line": 8084,
    "section": "Links"
  },
  {
    "markdown": "[foo`][ref]`\n\n[ref]: /uri\n",
    "html": "<p>[foo<code>][ref]</code></p>\n",
    "example": 533,
    "start_line": 8041,
    "end_line": 8047,
    "example": 536,
    "start_line": 8087,
    "end_line": 8093,
    "section": "Links"
  },
  {
    "markdown": "[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n",
    "html": "<p>[foo<a href=\"http://example.com/?search=%5D%5Bref%5D\">http://example.com/?search=][ref]</a></p>\n",
    "example": 534,
    "start_line": 8050,
    "end_line": 8056,
    "example": 537,
    "start_line": 8096,
    "end_line": 8102,
    "section": "Links"
  },
  {
    "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 535,
    "start_line": 8061,
    "end_line": 8067,
    "example": 538,
    "start_line": 8107,
    "end_line": 8113,
    "section": "Links"
  },
  {
    "markdown": "[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url\n",
    "html": "<p><a href=\"/url\">Толпой</a> is a Russian word.</p>\n",
    "example": 536,
    "start_line": 8072,
    "end_line": 8078,
    "markdown": "[ẞ]\n\n[SS]: /url\n",
    "html": "<p><a href=\"/url\"></a></p>\n",
    "example": 539,
    "start_line": 8118,
    "end_line": 8124,
    "section": "Links"
  },
  {
    "markdown": "[Foo\n  bar]: /url\n\n[Baz][Foo bar]\n",
    "html": "<p><a href=\"/url\">Baz</a></p>\n",
    "example": 537,
    "start_line": 8084,
    "end_line": 8091,
    "example": 540,
    "start_line": 8130,
    "end_line": 8137,
    "section": "Links"
  },
  {
    "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n",
    "html": "<p>[foo] <a href=\"/url\" title=\"title\">bar</a></p>\n",
    "example": 538,
    "start_line": 8097,
    "end_line": 8103,
    "example": 541,
    "start_line": 8143,
    "end_line": 8149,
    "section": "Links"
  },
  {
    "markdown": "[foo]\n[bar]\n\n[bar]: /url \"title\"\n",
    "html": "<p>[foo]\n<a href=\"/url\" title=\"title\">bar</a></p>\n",
    "example": 539,
    "start_line": 8106,
    "end_line": 8114,
    "example": 542,
    "start_line": 8152,
    "end_line": 8160,
    "section": "Links"
  },
  {
    "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n",
    "html": "<p><a href=\"/url1\">bar</a></p>\n",
    "example": 540,
    "start_line": 8147,
    "end_line": 8155,
    "example": 543,
    "start_line": 8193,
    "end_line": 8201,
    "section": "Links"
  },
  {
    "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n",
    "html": "<p>[bar][foo!]</p>\n",
    "example": 541,
    "start_line": 8162,
    "end_line": 8168,
    "example": 544,
    "start_line": 8208,
    "end_line": 8214,
    "section": "Links"
  },
  {
    "markdown": "[foo][ref[]\n\n[ref[]: /uri\n",
    "html": "<p>[foo][ref[]</p>\n<p>[ref[]: /uri</p>\n",
    "example": 542,
    "start_line": 8174,
    "end_line": 8181,
    "example": 545,
    "start_line": 8220,
    "end_line": 8227,
    "section": "Links"
  },
  {
    "markdown": "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n",
    "html": "<p>[foo][ref[bar]]</p>\n<p>[ref[bar]]: /uri</p>\n",
    "example": 543,
    "start_line": 8184,
    "end_line": 8191,
    "example": 546,
    "start_line": 8230,
    "end_line": 8237,
    "section": "Links"
  },
  {
    "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n",
    "html": "<p>[[[foo]]]</p>\n<p>[[[foo]]]: /url</p>\n",
    "example": 544,
    "start_line": 8194,
    "end_line": 8201,
    "example": 547,
    "start_line": 8240,
    "end_line": 8247,
    "section": "Links"
  },
  {
    "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n",
    "html": "<p><a href=\"/uri\">foo</a></p>\n",
    "example": 545,
    "start_line": 8204,
    "end_line": 8210,
    "example": 548,
    "start_line": 8250,
    "end_line": 8256,
    "section": "Links"
  },
  {
    "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n",
    "html": "<p><a href=\"/uri\">bar\\</a></p>\n",
    "example": 546,
    "start_line": 8215,
    "end_line": 8221,
    "example": 549,
    "start_line": 8261,
    "end_line": 8267,
    "section": "Links"
  },
  {
    "markdown": "[]\n\n[]: /uri\n",
    "html": "<p>[]</p>\n<p>[]: /uri</p>\n",
    "example": 547,
    "start_line": 8226,
    "end_line": 8233,
    "example": 550,
    "start_line": 8273,
    "end_line": 8280,
    "section": "Links"
  },
  {
    "markdown": "[\n ]\n\n[\n ]: /uri\n",
    "html": "<p>[\n]</p>\n<p>[\n]: /uri</p>\n",
    "example": 548,
    "start_line": 8236,
    "end_line": 8247,
    "example": 551,
    "start_line": 8283,
    "end_line": 8294,
    "section": "Links"
  },
  {
    "markdown": "[foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 549,
    "start_line": 8259,
    "end_line": 8265,
    "example": 552,
    "start_line": 8306,
    "end_line": 8312,
    "section": "Links"
  },
  {
    "markdown": "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\"><em>foo</em> bar</a></p>\n",
    "example": 550,
    "start_line": 8268,
    "end_line": 8274,
    "example": 553,
    "start_line": 8315,
    "end_line": 8321,
    "section": "Links"
  },
  {
    "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n",
    "example": 551,
    "start_line": 8279,
    "end_line": 8285,
    "example": 554,
    "start_line": 8326,
    "end_line": 8332,
    "section": "Links"
  },
  {
    "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a>\n[]</p>\n",
    "example": 552,
    "start_line": 8292,
    "end_line": 8300,
    "example": 555,
    "start_line": 8339,
    "end_line": 8347,
    "section": "Links"
  },
  {
    "markdown": "[foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 553,
    "start_line": 8312,
    "end_line": 8318,
    "example": 556,
    "start_line": 8359,
    "end_line": 8365,
    "section": "Links"
  },
  {
    "markdown": "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\"><em>foo</em> bar</a></p>\n",
    "example": 554,
    "start_line": 8321,
    "end_line": 8327,
    "example": 557,
    "start_line": 8368,
    "end_line": 8374,
    "section": "Links"
  },
  {
    "markdown": "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n",
    "html": "<p>[<a href=\"/url\" title=\"title\"><em>foo</em> bar</a>]</p>\n",
    "example": 555,
    "start_line": 8330,
    "end_line": 8336,
    "example": 558,
    "start_line": 8377,
    "end_line": 8383,
    "section": "Links"
  },
  {
    "markdown": "[[bar [foo]\n\n[foo]: /url\n",
    "html": "<p>[[bar <a href=\"/url\">foo</a></p>\n",
    "example": 556,
    "start_line": 8339,
    "end_line": 8345,
    "example": 559,
    "start_line": 8386,
    "end_line": 8392,
    "section": "Links"
  },
  {
    "markdown": "[Foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n",
    "example": 557,
    "start_line": 8350,
    "end_line": 8356,
    "example": 560,
    "start_line": 8397,
    "end_line": 8403,
    "section": "Links"
  },
  {
    "markdown": "[foo] bar\n\n[foo]: /url\n",
    "html": "<p><a href=\"/url\">foo</a> bar</p>\n",
    "example": 558,
    "start_line": 8361,
    "end_line": 8367,
    "example": 561,
    "start_line": 8408,
    "end_line": 8414,
    "section": "Links"
  },
  {
    "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p>[foo]</p>\n",
    "example": 559,
    "start_line": 8373,
    "end_line": 8379,
    "example": 562,
    "start_line": 8420,
    "end_line": 8426,
    "section": "Links"
  },
  {
    "markdown": "[foo*]: /url\n\n*[foo*]\n",
    "html": "<p>*<a href=\"/url\">foo*</a></p>\n",
    "example": 560,
    "start_line": 8385,
    "end_line": 8391,
    "example": 563,
    "start_line": 8432,
    "end_line": 8438,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n",
    "html": "<p><a href=\"/url2\">foo</a></p>\n",
    "example": 561,
    "start_line": 8397,
    "end_line": 8404,
    "example": 564,
    "start_line": 8444,
    "end_line": 8451,
    "section": "Links"
  },
  {
    "markdown": "[foo][]\n\n[foo]: /url1\n",
    "html": "<p><a href=\"/url1\">foo</a></p>\n",
    "example": 562,
    "start_line": 8406,
    "end_line": 8412,
    "example": 565,
    "start_line": 8453,
    "end_line": 8459,
    "section": "Links"
  },
  {
    "markdown": "[foo]()\n\n[foo]: /url1\n",
    "html": "<p><a href=\"\">foo</a></p>\n",
    "example": 563,
    "start_line": 8416,
    "end_line": 8422,
    "example": 566,
    "start_line": 8463,
    "end_line": 8469,
    "section": "Links"
  },
  {
    "markdown": "[foo](not a link)\n\n[foo]: /url1\n",
    "html": "<p><a href=\"/url1\">foo</a>(not a link)</p>\n",
    "example": 564,
    "start_line": 8424,
    "end_line": 8430,
    "example": 567,
    "start_line": 8471,
    "end_line": 8477,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar][baz]\n\n[baz]: /url\n",
    "html": "<p>[foo]<a href=\"/url\">bar</a></p>\n",
    "example": 565,
    "start_line": 8435,
    "end_line": 8441,
    "example": 568,
    "start_line": 8482,
    "end_line": 8488,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n",
    "html": "<p><a href=\"/url2\">foo</a><a href=\"/url1\">baz</a></p>\n",
    "example": 566,
    "start_line": 8447,
    "end_line": 8454,
    "example": 569,
    "start_line": 8494,
    "end_line": 8501,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n",
    "html": "<p>[foo]<a href=\"/url1\">bar</a></p>\n",
    "example": 567,
    "start_line": 8460,
    "end_line": 8467,
    "example": 570,
    "start_line": 8507,
    "end_line": 8514,
    "section": "Links"
  },
  {
    "markdown": "![foo](/url \"title\")\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n",
    "example": 568,
    "start_line": 8483,
    "end_line": 8487,
    "example": 571,
    "start_line": 8530,
    "end_line": 8534,
    "section": "Images"
  },
  {
    "markdown": "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n",
    "html": "<p><img src=\"train.jpg\" alt=\"foo bar\" title=\"train &amp; tracks\" /></p>\n",
    "example": 569,
    "start_line": 8490,
    "end_line": 8496,
    "example": 572,
    "start_line": 8537,
    "end_line": 8543,
    "section": "Images"
  },
  {
    "markdown": "![foo ![bar](/url)](/url2)\n",
    "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n",
    "example": 570,
    "start_line": 8499,
    "end_line": 8503,
    "example": 573,
    "start_line": 8546,
    "end_line": 8550,
    "section": "Images"
  },
  {
    "markdown": "![foo [bar](/url)](/url2)\n",
    "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n",
    "example": 571,
    "start_line": 8506,
    "end_line": 8510,
    "example": 574,
    "start_line": 8553,
    "end_line": 8557,
    "section": "Images"
  },
  {
    "markdown": "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n",
    "html": "<p><img src=\"train.jpg\" alt=\"foo bar\" title=\"train &amp; tracks\" /></p>\n",
    "example": 572,
    "start_line": 8520,
    "end_line": 8526,
    "example": 575,
    "start_line": 8567,
    "end_line": 8573,
    "section": "Images"
  },
  {
    "markdown": "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n",
    "html": "<p><img src=\"train.jpg\" alt=\"foo bar\" title=\"train &amp; tracks\" /></p>\n",
    "example": 573,
    "start_line": 8529,
    "end_line": 8535,
    "example": 576,
    "start_line": 8576,
    "end_line": 8582,
    "section": "Images"
  },
  {
    "markdown": "![foo](train.jpg)\n",
    "html": "<p><img src=\"train.jpg\" alt=\"foo\" /></p>\n",
    "example": 574,
    "start_line": 8538,
    "end_line": 8542,
    "example": 577,
    "start_line": 8585,
    "end_line": 8589,
    "section": "Images"
  },
  {
    "markdown": "My ![foo bar](/path/to/train.jpg  \"title\"   )\n",
    "html": "<p>My <img src=\"/path/to/train.jpg\" alt=\"foo bar\" title=\"title\" /></p>\n",
    "example": 575,
    "start_line": 8545,
    "end_line": 8549,
    "example": 578,
    "start_line": 8592,
    "end_line": 8596,
    "section": "Images"
  },
  {
    "markdown": "![foo](<url>)\n",
    "html": "<p><img src=\"url\" alt=\"foo\" /></p>\n",
    "example": 576,
    "start_line": 8552,
    "end_line": 8556,
    "example": 579,
    "start_line": 8599,
    "end_line": 8603,
    "section": "Images"
  },
  {
    "markdown": "![](/url)\n",
    "html": "<p><img src=\"/url\" alt=\"\" /></p>\n",
    "example": 577,
    "start_line": 8559,
    "end_line": 8563,
    "example": 580,
    "start_line": 8606,
    "end_line": 8610,
    "section": "Images"
  },
  {
    "markdown": "![foo][bar]\n\n[bar]: /url\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n",
    "example": 578,
    "start_line": 8568,
    "end_line": 8574,
    "example": 581,
    "start_line": 8615,
    "end_line": 8621,
    "section": "Images"
  },
  {
    "markdown": "![foo][bar]\n\n[BAR]: /url\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n",
    "example": 579,
    "start_line": 8577,
    "end_line": 8583,
    "example": 582,
    "start_line": 8624,
    "end_line": 8630,
    "section": "Images"
  },
  {
    "markdown": "![foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n",
    "example": 580,
    "start_line": 8588,
    "end_line": 8594,
    "example": 583,
    "start_line": 8635,
    "end_line": 8641,
    "section": "Images"
  },
  {
    "markdown": "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"foo bar\" title=\"title\" /></p>\n",
    "example": 581,
    "start_line": 8597,
    "end_line": 8603,
    "example": 584,
    "start_line": 8644,
    "end_line": 8650,
    "section": "Images"
  },
  {
    "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n",
    "example": 582,
    "start_line": 8608,
    "end_line": 8614,
    "example": 585,
    "start_line": 8655,
    "end_line": 8661,
    "section": "Images"
  },
  {
    "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" />\n[]</p>\n",
    "example": 583,
    "start_line": 8620,
    "end_line": 8628,
    "example": 586,
    "start_line": 8667,
    "end_line": 8675,
    "section": "Images"
  },
  {
    "markdown": "![foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n",
    "example": 584,
    "start_line": 8633,
    "end_line": 8639,
    "example": 587,
    "start_line": 8680,
    "end_line": 8686,
    "section": "Images"
  },
  {
    "markdown": "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"foo bar\" title=\"title\" /></p>\n",
    "example": 585,
    "start_line": 8642,
    "end_line": 8648,
    "example": 588,
    "start_line": 8689,
    "end_line": 8695,
    "section": "Images"
  },
  {
    "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n",
    "html": "<p>![[foo]]</p>\n<p>[[foo]]: /url &quot;title&quot;</p>\n",
    "example": 586,
    "start_line": 8653,
    "end_line": 8660,
    "example": 589,
    "start_line": 8700,
    "end_line": 8707,
    "section": "Images"
  },
  {
    "markdown": "![Foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n",
    "example": 587,
    "start_line": 8665,
    "end_line": 8671,
    "example": 590,
    "start_line": 8712,
    "end_line": 8718,
    "section": "Images"
  },
  {
    "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p>![foo]</p>\n",
    "example": 588,
    "start_line": 8677,
    "end_line": 8683,
    "example": 591,
    "start_line": 8724,
    "end_line": 8730,
    "section": "Images"
  },
  {
    "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p>!<a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 589,
    "start_line": 8689,
    "end_line": 8695,
    "example": 592,
    "start_line": 8736,
    "end_line": 8742,
    "section": "Images"
  },
  {
    "markdown": "<http://foo.bar.baz>\n",
    "html": "<p><a href=\"http://foo.bar.baz\">http://foo.bar.baz</a></p>\n",
    "example": 590,
    "start_line": 8722,
    "end_line": 8726,
    "example": 593,
    "start_line": 8769,
    "end_line": 8773,
    "section": "Autolinks"
  },
  {
    "markdown": "<http://foo.bar.baz/test?q=hello&id=22&boolean>\n",
    "html": "<p><a href=\"http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean\">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>\n",
    "example": 591,
    "start_line": 8729,
    "end_line": 8733,
    "example": 594,
    "start_line": 8776,
    "end_line": 8780,
    "section": "Autolinks"
  },
  {
    "markdown": "<irc://foo.bar:2233/baz>\n",
    "html": "<p><a href=\"irc://foo.bar:2233/baz\">irc://foo.bar:2233/baz</a></p>\n",
    "example": 592,
    "start_line": 8736,
    "end_line": 8740,
    "example": 595,
    "start_line": 8783,
    "end_line": 8787,
    "section": "Autolinks"
  },
  {
    "markdown": "<MAILTO:FOO@BAR.BAZ>\n",
    "html": "<p><a href=\"MAILTO:FOO@BAR.BAZ\">MAILTO:FOO@BAR.BAZ</a></p>\n",
    "example": 593,
    "start_line": 8745,
    "end_line": 8749,
    "example": 596,
    "start_line": 8792,
    "end_line": 8796,
    "section": "Autolinks"
  },
  {
    "markdown": "<a+b+c:d>\n",
    "html": "<p><a href=\"a+b+c:d\">a+b+c:d</a></p>\n",
    "example": 594,
    "start_line": 8757,
    "end_line": 8761,
    "example": 597,
    "start_line": 8804,
    "end_line": 8808,
    "section": "Autolinks"
  },
  {
    "markdown": "<made-up-scheme://foo,bar>\n",
    "html": "<p><a href=\"made-up-scheme://foo,bar\">made-up-scheme://foo,bar</a></p>\n",
    "example": 595,
    "start_line": 8764,
    "end_line": 8768,
    "example": 598,
    "start_line": 8811,
    "end_line": 8815,
    "section": "Autolinks"
  },
  {
    "markdown": "<http://../>\n",
    "html": "<p><a href=\"http://../\">http://../</a></p>\n",
    "example": 596,
    "start_line": 8771,
    "end_line": 8775,
    "example": 599,
    "start_line": 8818,
    "end_line": 8822,
    "section": "Autolinks"
  },
  {
    "markdown": "<localhost:5001/foo>\n",
    "html": "<p><a href=\"localhost:5001/foo\">localhost:5001/foo</a></p>\n",
    "example": 597,
    "start_line": 8778,
    "end_line": 8782,
    "example": 600,
    "start_line": 8825,
    "end_line": 8829,
    "section": "Autolinks"
  },
  {
    "markdown": "<http://foo.bar/baz bim>\n",
    "html": "<p>&lt;http://foo.bar/baz bim&gt;</p>\n",
    "example": 598,
    "start_line": 8787,
    "end_line": 8791,
    "example": 601,
    "start_line": 8834,
    "end_line": 8838,
    "section": "Autolinks"
  },
  {
    "markdown": "<http://example.com/\\[\\>\n",
    "html": "<p><a href=\"http://example.com/%5C%5B%5C\">http://example.com/\\[\\</a></p>\n",
    "example": 599,
    "start_line": 8796,
    "end_line": 8800,
    "example": 602,
    "start_line": 8843,
    "end_line": 8847,
    "section": "Autolinks"
  },
  {
    "markdown": "<foo@bar.example.com>\n",
    "html": "<p><a href=\"mailto:foo@bar.example.com\">foo@bar.example.com</a></p>\n",
    "example": 600,
    "start_line": 8818,
    "end_line": 8822,
    "example": 603,
    "start_line": 8865,
    "end_line": 8869,
    "section": "Autolinks"
  },
  {
    "markdown": "<foo+special@Bar.baz-bar0.com>\n",
    "html": "<p><a href=\"mailto:foo+special@Bar.baz-bar0.com\">foo+special@Bar.baz-bar0.com</a></p>\n",
    "example": 601,
    "start_line": 8825,
    "end_line": 8829,
    "example": 604,
    "start_line": 8872,
    "end_line": 8876,
    "section": "Autolinks"
  },
  {
    "markdown": "<foo\\+@bar.example.com>\n",
    "html": "<p>&lt;foo+@bar.example.com&gt;</p>\n",
    "example": 602,
    "start_line": 8834,
    "end_line": 8838,
    "example": 605,
    "start_line": 8881,
    "end_line": 8885,
    "section": "Autolinks"
  },
  {
    "markdown": "<>\n",
    "html": "<p>&lt;&gt;</p>\n",
    "example": 603,
    "start_line": 8843,
    "end_line": 8847,
    "example": 606,
    "start_line": 8890,
    "end_line": 8894,
    "section": "Autolinks"
  },
  {
    "markdown": "< http://foo.bar >\n",
    "html": "<p>&lt; http://foo.bar &gt;</p>\n",
    "example": 604,
    "start_line": 8850,
    "end_line": 8854,
    "example": 607,
    "start_line": 8897,
    "end_line": 8901,
    "section": "Autolinks"
  },
  {
    "markdown": "<m:abc>\n",
    "html": "<p>&lt;m:abc&gt;</p>\n",
    "example": 605,
    "start_line": 8857,
    "end_line": 8861,
    "example": 608,
    "start_line": 8904,
    "end_line": 8908,
    "section": "Autolinks"
  },
  {
    "markdown": "<foo.bar.baz>\n",
    "html": "<p>&lt;foo.bar.baz&gt;</p>\n",
    "example": 606,
    "start_line": 8864,
    "end_line": 8868,
    "example": 609,
    "start_line": 8911,
    "end_line": 8915,
    "section": "Autolinks"
  },
  {
    "markdown": "http://example.com\n",
    "html": "<p>http://example.com</p>\n",
    "example": 607,
    "start_line": 8871,
    "end_line": 8875,
    "example": 610,
    "start_line": 8918,
    "end_line": 8922,
    "section": "Autolinks"
  },
  {
    "markdown": "foo@bar.example.com\n",
    "html": "<p>foo@bar.example.com</p>\n",
    "example": 608,
    "start_line": 8878,
    "end_line": 8882,
    "example": 611,
    "start_line": 8925,
    "end_line": 8929,
    "section": "Autolinks"
  },
  {
    "markdown": "<a><bab><c2c>\n",
    "html": "<p><a><bab><c2c></p>\n",
    "example": 609,
    "start_line": 8960,
    "end_line": 8964,
    "example": 612,
    "start_line": 9006,
    "end_line": 9010,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a/><b2/>\n",
    "html": "<p><a/><b2/></p>\n",
    "example": 610,
    "start_line": 8969,
    "end_line": 8973,
    "example": 613,
    "start_line": 9015,
    "end_line": 9019,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a  /><b2\ndata=\"foo\" >\n",
    "html": "<p><a  /><b2\ndata=\"foo\" ></p>\n",
    "example": 611,
    "start_line": 8978,
    "end_line": 8984,
    "example": 614,
    "start_line": 9024,
    "end_line": 9030,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a foo=\"bar\" bam = 'baz <em>\"</em>'\n_boolean zoop:33=zoop:33 />\n",
    "html": "<p><a foo=\"bar\" bam = 'baz <em>\"</em>'\n_boolean zoop:33=zoop:33 /></p>\n",
    "example": 612,
    "start_line": 8989,
    "end_line": 8995,
    "example": 615,
    "start_line": 9035,
    "end_line": 9041,
    "section": "Raw HTML"
  },
  {
    "markdown": "Foo <responsive-image src=\"foo.jpg\" />\n",
    "html": "<p>Foo <responsive-image src=\"foo.jpg\" /></p>\n",
    "example": 613,
    "start_line": 9000,
    "end_line": 9004,
    "example": 616,
    "start_line": 9046,
    "end_line": 9050,
    "section": "Raw HTML"
  },
  {
    "markdown": "<33> <__>\n",
    "html": "<p>&lt;33&gt; &lt;__&gt;</p>\n",
    "example": 614,
    "start_line": 9009,
    "end_line": 9013,
    "example": 617,
    "start_line": 9055,
    "end_line": 9059,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a h*#ref=\"hi\">\n",
    "html": "<p>&lt;a h*#ref=&quot;hi&quot;&gt;</p>\n",
    "example": 615,
    "start_line": 9018,
    "end_line": 9022,
    "example": 618,
    "start_line": 9064,
    "end_line": 9068,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a href=\"hi'> <a href=hi'>\n",
    "html": "<p>&lt;a href=&quot;hi'&gt; &lt;a href=hi'&gt;</p>\n",
    "example": 616,
    "start_line": 9027,
    "end_line": 9031,
    "example": 619,
    "start_line": 9073,
    "end_line": 9077,
    "section": "Raw HTML"
  },
  {
    "markdown": "< a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop />\n",
    "html": "<p>&lt; a&gt;&lt;\nfoo&gt;&lt;bar/ &gt;\n&lt;foo bar=baz\nbim!bop /&gt;</p>\n",
    "example": 617,
    "start_line": 9036,
    "end_line": 9046,
    "example": 620,
    "start_line": 9082,
    "end_line": 9092,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a href='bar'title=title>\n",
    "html": "<p>&lt;a href='bar'title=title&gt;</p>\n",
    "example": 618,
    "start_line": 9051,
    "end_line": 9055,
    "example": 621,
    "start_line": 9097,
    "end_line": 9101,
    "section": "Raw HTML"
  },
  {
    "markdown": "</a></foo >\n",
    "html": "<p></a></foo ></p>\n",
    "example": 619,
    "start_line": 9060,
    "end_line": 9064,
    "example": 622,
    "start_line": 9106,
    "end_line": 9110,
    "section": "Raw HTML"
  },
  {
    "markdown": "</a href=\"foo\">\n",
    "html": "<p>&lt;/a href=&quot;foo&quot;&gt;</p>\n",
    "example": 620,
    "start_line": 9069,
    "end_line": 9073,
    "example": 623,
    "start_line": 9115,
    "end_line": 9119,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <!-- this is a\ncomment - with hyphen -->\n",
    "html": "<p>foo <!-- this is a\ncomment - with hyphen --></p>\n",
    "example": 621,
    "start_line": 9078,
    "end_line": 9084,
    "example": 624,
    "start_line": 9124,
    "end_line": 9130,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <!-- not a comment -- two hyphens -->\n",
    "html": "<p>foo &lt;!-- not a comment -- two hyphens --&gt;</p>\n",
    "example": 622,
    "start_line": 9087,
    "end_line": 9091,
    "example": 625,
    "start_line": 9133,
    "end_line": 9137,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <!--> foo -->\n\nfoo <!-- foo--->\n",
    "html": "<p>foo &lt;!--&gt; foo --&gt;</p>\n<p>foo &lt;!-- foo---&gt;</p>\n",
    "example": 623,
    "start_line": 9096,
    "end_line": 9103,
    "example": 626,
    "start_line": 9142,
    "end_line": 9149,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <?php echo $a; ?>\n",
    "html": "<p>foo <?php echo $a; ?></p>\n",
    "example": 624,
    "start_line": 9108,
    "end_line": 9112,
    "example": 627,
    "start_line": 9154,
    "end_line": 9158,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <!ELEMENT br EMPTY>\n",
    "html": "<p>foo <!ELEMENT br EMPTY></p>\n",
    "example": 625,
    "start_line": 9117,
    "end_line": 9121,
    "example": 628,
    "start_line": 9163,
    "end_line": 9167,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <![CDATA[>&<]]>\n",
    "html": "<p>foo <![CDATA[>&<]]></p>\n",
    "example": 626,
    "start_line": 9126,
    "end_line": 9130,
    "example": 629,
    "start_line": 9172,
    "end_line": 9176,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <a href=\"&ouml;\">\n",
    "html": "<p>foo <a href=\"&ouml;\"></p>\n",
    "example": 627,
    "start_line": 9136,
    "end_line": 9140,
    "example": 630,
    "start_line": 9182,
    "end_line": 9186,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <a href=\"\\*\">\n",
    "html": "<p>foo <a href=\"\\*\"></p>\n",
    "example": 628,
    "start_line": 9145,
    "end_line": 9149,
    "example": 631,
    "start_line": 9191,
    "end_line": 9195,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a href=\"\\\"\">\n",
    "html": "<p>&lt;a href=&quot;&quot;&quot;&gt;</p>\n",
    "example": 629,
    "start_line": 9152,
    "end_line": 9156,
    "example": 632,
    "start_line": 9198,
    "end_line": 9202,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo  \nbaz\n",
    "html": "<p>foo<br />\nbaz</p>\n",
    "example": 630,
    "start_line": 9166,
    "end_line": 9172,
    "example": 633,
    "start_line": 9212,
    "end_line": 9218,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\\\nbaz\n",
    "html": "<p>foo<br />\nbaz</p>\n",
    "example": 631,
    "start_line": 9178,
    "end_line": 9184,
    "example": 634,
    "start_line": 9224,
    "end_line": 9230,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo       \nbaz\n",
    "html": "<p>foo<br />\nbaz</p>\n",
    "example": 632,
    "start_line": 9189,
    "end_line": 9195,
    "example": 635,
    "start_line": 9235,
    "end_line": 9241,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo  \n     bar\n",
    "html": "<p>foo<br />\nbar</p>\n",
    "example": 633,
    "start_line": 9200,
    "end_line": 9206,
    "example": 636,
    "start_line": 9246,
    "end_line": 9252,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\\\n     bar\n",
    "html": "<p>foo<br />\nbar</p>\n",
    "example": 634,
    "start_line": 9209,
    "end_line": 9215,
    "example": 637,
    "start_line": 9255,
    "end_line": 9261,
    "section": "Hard line breaks"
  },
  {
    "markdown": "*foo  \nbar*\n",
    "html": "<p><em>foo<br />\nbar</em></p>\n",
    "example": 635,
    "start_line": 9221,
    "end_line": 9227,
    "example": 638,
    "start_line": 9267,
    "end_line": 9273,
    "section": "Hard line breaks"
  },
  {
    "markdown": "*foo\\\nbar*\n",
    "html": "<p><em>foo<br />\nbar</em></p>\n",
    "example": 636,
    "start_line": 9230,
    "end_line": 9236,
    "example": 639,
    "start_line": 9276,
    "end_line": 9282,
    "section": "Hard line breaks"
  },
  {
    "markdown": "`code \nspan`\n",
    "html": "<p><code>code  span</code></p>\n",
    "example": 637,
    "start_line": 9241,
    "end_line": 9246,
    "markdown": "`code  \nspan`\n",
    "html": "<p><code>code   span</code></p>\n",
    "example": 640,
    "start_line": 9287,
    "end_line": 9292,
    "section": "Hard line breaks"
  },
  {
    "markdown": "`code\\\nspan`\n",
    "html": "<p><code>code\\ span</code></p>\n",
    "example": 638,
    "start_line": 9249,
    "end_line": 9254,
    "example": 641,
    "start_line": 9295,
    "end_line": 9300,
    "section": "Hard line breaks"
  },
  {
    "markdown": "<a href=\"foo  \nbar\">\n",
    "html": "<p><a href=\"foo  \nbar\"></p>\n",
    "example": 639,
    "start_line": 9259,
    "end_line": 9265,
    "example": 642,
    "start_line": 9305,
    "end_line": 9311,
    "section": "Hard line breaks"
  },
  {
    "markdown": "<a href=\"foo\\\nbar\">\n",
    "html": "<p><a href=\"foo\\\nbar\"></p>\n",
    "example": 640,
    "start_line": 9268,
    "end_line": 9274,
    "example": 643,
    "start_line": 9314,
    "end_line": 9320,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\\\n",
    "html": "<p>foo\\</p>\n",
    "example": 641,
    "start_line": 9281,
    "end_line": 9285,
    "example": 644,
    "start_line": 9327,
    "end_line": 9331,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo  \n",
    "html": "<p>foo</p>\n",
    "example": 642,
    "start_line": 9288,
    "end_line": 9292,
    "example": 645,
    "start_line": 9334,
    "end_line": 9338,
    "section": "Hard line breaks"
  },
  {
    "markdown": "### foo\\\n",
    "html": "<h3>foo\\</h3>\n",
    "example": 643,
    "start_line": 9295,
    "end_line": 9299,
    "example": 646,
    "start_line": 9341,
    "end_line": 9345,
    "section": "Hard line breaks"
  },
  {
    "markdown": "### foo  \n",
    "html": "<h3>foo</h3>\n",
    "example": 644,
    "start_line": 9302,
    "end_line": 9306,
    "example": 647,
    "start_line": 9348,
    "end_line": 9352,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\nbaz\n",
    "html": "<p>foo\nbaz</p>\n",
    "example": 645,
    "start_line": 9317,
    "end_line": 9323,
    "example": 648,
    "start_line": 9363,
    "end_line": 9369,
    "section": "Soft line breaks"
  },
  {
    "markdown": "foo \n baz\n",
    "html": "<p>foo\nbaz</p>\n",
    "example": 646,
    "start_line": 9329,
    "end_line": 9335,
    "example": 649,
    "start_line": 9375,
    "end_line": 9381,
    "section": "Soft line breaks"
  },
  {
    "markdown": "hello $.;'there\n",
    "html": "<p>hello $.;'there</p>\n",
    "example": 647,
    "start_line": 9349,
    "end_line": 9353,
    "example": 650,
    "start_line": 9395,
    "end_line": 9399,
    "section": "Textual content"
  },
  {
    "markdown": "Foo χρῆν\n",
    "html": "<p>Foo χρῆν</p>\n",
    "example": 648,
    "start_line": 9356,
    "end_line": 9360,
    "example": 651,
    "start_line": 9402,
    "end_line": 9406,
    "section": "Textual content"
  },
  {
    "markdown": "Multiple     spaces\n",
    "html": "<p>Multiple     spaces</p>\n",
    "example": 649,
    "start_line": 9365,
    "end_line": 9369,
    "example": 652,
    "start_line": 9411,
    "end_line": 9415,
    "section": "Textual content"
  }
]

Changes to testdata/meta/title/20200310110300.zettel.

1


1
-
+
title: A ""Title"" with //Markup//, ``Zettelmarkup``{=zmk}
title: A ""Title"" with __Markup__, ``Zettelmarkup``{=zmk}

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/naughty/LICENSE.























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The MIT License (MIT)

Copyright (c) 2015-2020 Max Woolf

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.

Added testdata/naughty/README.md.







1
2
3
4
5
6
+
+
+
+
+
+
# Big List of Naughty Strings

A list of strings which have a high probability of causing issues when used as user-input data.

* Source: https://github.com/minimaxir/big-list-of-naughty-strings
* License: MIT, (c) 2015-2020 Max Woolf (see file LICENSE)

Added testdata/naughty/blns.txt.







































































































































































































































































































































































































































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
#	Reserved Strings
#
#	Strings which may be used elsewhere in code

undefined
undef
null
NULL
(null)
nil
NIL
true
false
True
False
TRUE
FALSE
None
hasOwnProperty
then
constructor
\
\\

#	Numeric Strings
#
#	Strings which can be interpreted as numeric

0
1
1.00
$1.00
1/2
1E2
1E02
1E+02
-1
-1.00
-$1.00
-1/2
-1E2
-1E02
-1E+02
1/0
0/0
-2147483648/-1
-9223372036854775808/-1
-0
-0.0
+0
+0.0
0.00
0..0
.
0.0.0
0,00
0,,0
,
0,0,0
0.0/0
1.0/0.0
0.0/0.0
1,0/0,0
0,0/0,0
--1
-
-.
-,
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
NaN
Infinity
-Infinity
INF
1#INF
-1#IND
1#QNAN
1#SNAN
1#IND
0x0
0xffffffff
0xffffffffffffffff
0xabad1dea
123456789012345678901234567890123456789
1,000.00
1 000.00
1'000.00
1,000,000.00
1 000 000.00
1'000'000.00
1.000,00
1 000,00
1'000,00
1.000.000,00
1 000 000,00
1'000'000,00
01000
08
09
2.2250738585072011e-308

#	Special Characters
#
# ASCII punctuation.  All of these characters may need to be escaped in some
# contexts.  Divided into three groups based on (US-layout) keyboard position.

,./;'[]\-=
<>?:"{}|_+
!@#$%^&*()`~

# Non-whitespace C0 controls: U+0001 through U+0008, U+000E through U+001F,
# and U+007F (DEL)
# Often forbidden to appear in various text-based file formats (e.g. XML),
# or reused for internal delimiters on the theory that they should never
# appear in input.
# The next line may appear to be blank or mojibake in some viewers.


# Non-whitespace C1 controls: U+0080 through U+0084 and U+0086 through U+009F.
# Commonly misinterpreted as additional graphic characters.
# The next line may appear to be blank, mojibake, or dingbats in some viewers.
€‚ƒ„†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ

# Whitespace: all of the characters with category Zs, Zl, or Zp (in Unicode
# version 8.0.0), plus U+0009 (HT), U+000B (VT), U+000C (FF), U+0085 (NEL),
# and U+200B (ZERO WIDTH SPACE), which are in the C categories but are often
# treated as whitespace in some contexts.
# This file unfortunately cannot express strings containing
# U+0000, U+000A, or U+000D (NUL, LF, CR).
# The next line may appear to be blank or mojibake in some viewers.
# The next line may be flagged for "trailing whitespace" in some viewers.
	 …             ​

   

# Unicode additional control characters: all of the characters with
# general category Cf (in Unicode 8.0.0).
# The next line may appear to be blank or mojibake in some viewers.
­؀؁؂؃؄؅؜۝܏᠎​‌‍‎‏‪‫‬‭‮⁠⁡⁢⁣⁤⁦⁧⁨⁩𑂽𛲠𛲡𛲢𛲣𝅳𝅴𝅵𝅶𝅷𝅸𝅹𝅺󠀁󠀠󠀡󠀢󠀣󠀤󠀥󠀦󠀧󠀨󠀩󠀪󠀫󠀬󠀭󠀮󠀯󠀰󠀱󠀲󠀳󠀴󠀵󠀶󠀷󠀸󠀹󠀺󠀻󠀼󠀽󠀾󠀿󠁀󠁁󠁂󠁃󠁄󠁅󠁆󠁇󠁈󠁉󠁊󠁋󠁌󠁍󠁎󠁏󠁐󠁑󠁒󠁓󠁔󠁕󠁖󠁗󠁘󠁙󠁚󠁛󠁜󠁝󠁞󠁟󠁠󠁡󠁢󠁣󠁤󠁥󠁦󠁧󠁨󠁩󠁪󠁫󠁬󠁭󠁮󠁯󠁰󠁱󠁲󠁳󠁴󠁵󠁶󠁷󠁸󠁹󠁺󠁻󠁼󠁽󠁾󠁿

# "Byte order marks", U+FEFF and U+FFFE, each on its own line.
# The next two lines may appear to be blank or mojibake in some viewers.



#	Unicode Symbols
#
#	Strings which contain common unicode symbols (e.g. smart quotes)

Ω≈ç√∫˜µ≤≥÷
åß∂ƒ©˙∆˚¬…æ
œ∑´®†¥¨ˆøπ“‘
¡™£¢∞§¶•ªº–≠
¸˛Ç◊ı˜Â¯˘¿
ÅÍÎÏ˝ÓÔÒÚÆ☃
Œ„´‰ˇÁ¨ˆØ∏”’
`⁄€‹›fifl‡°·‚—±
⅛⅜⅝⅞
ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя
٠١٢٣٤٥٦٧٨٩

#	Unicode Subscript/Superscript/Accents
#
#	Strings which contain unicode subscripts/superscripts; can cause rendering issues

⁰⁴⁵
₀₁₂
⁰⁴⁵₀₁₂
ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็

#	Quotation Marks
#
#	Strings which contain misplaced quotation marks; can cause encoding errors

'
"
''
""
'"'
"''''"'"
"'"'"''''"
<foo val=“bar” />
<foo val=“bar” />
<foo val=”bar“ />
<foo val=`bar' />

#	Two-Byte Characters
#
#	Strings which contain two-byte characters: can cause rendering issues or character-length issues

田中さんにあげて下さい
パーティーへ行かないか
和製漢語
部落格
사회과학원 어학연구소
찦차를 타고 온 펲시맨과 쑛다리 똠방각하
社會科學院語學研究所
울란바토르
𠜎𠜱𠝹𠱓𠱸𠲖𠳏

#	Strings which contain two-byte letters: can cause issues with naïve UTF-16 capitalizers which think that 16 bits == 1 character

𐐜 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐙𐐊𐐡𐐝𐐓/𐐝𐐇𐐗𐐊𐐤𐐔 𐐒𐐋𐐗 𐐒𐐌 𐐜 𐐡𐐀𐐖𐐇𐐤𐐓𐐝 𐐱𐑂 𐑄 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐏𐐆𐐅𐐤𐐆𐐚𐐊𐐡𐐝𐐆𐐓𐐆

#	Special Unicode Characters Union
#
#	A super string recommended by VMware Inc. Globalization Team: can effectively cause rendering issues or character-length issues to validate product globalization readiness.
#
#	表          CJK_UNIFIED_IDEOGRAPHS (U+8868)
#	ポ          KATAKANA LETTER PO (U+30DD)
#	あ          HIRAGANA LETTER A (U+3042)
#	A           LATIN CAPITAL LETTER A (U+0041)
#	鷗          CJK_UNIFIED_IDEOGRAPHS (U+9DD7)
#	Π          LATIN SMALL LIGATURE OE (U+0153) 
#	é           LATIN SMALL LETTER E WITH ACUTE (U+00E9)
#	B           FULLWIDTH LATIN CAPITAL LETTER B (U+FF22)
#	逍          CJK_UNIFIED_IDEOGRAPHS (U+900D)
#	Ü           LATIN SMALL LETTER U WITH DIAERESIS (U+00FC)
#	ß           LATIN SMALL LETTER SHARP S (U+00DF)
#	ª           FEMININE ORDINAL INDICATOR (U+00AA)
#	ą           LATIN SMALL LETTER A WITH OGONEK (U+0105)
#	ñ           LATIN SMALL LETTER N WITH TILDE (U+00F1)
#	丂          CJK_UNIFIED_IDEOGRAPHS (U+4E02)
#	㐀          CJK Ideograph Extension A, First (U+3400)
#	𠀀          CJK Ideograph Extension B, First (U+20000)

表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀

#	Changing length when lowercased
#
#	Characters which increase in length (2 to 3 bytes) when lowercased
#	Credit: https://twitter.com/jifa/status/625776454479970304

Ⱥ
Ⱦ

#	Japanese Emoticons
#
#	Strings which consists of Japanese-style emoticons which are popular on the web

ヽ༼ຈل͜ຈ༽ノ ヽ༼ຈل͜ຈ༽ノ
(。◕ ∀ ◕。)
`ィ(´∀`∩
__ロ(,_,*)
・( ̄∀ ̄)・:*:
゚・✿ヾ╲(。◕‿◕。)╱✿・゚
,。・:*:・゜’( ☻ ω ☻ )。・:*:・゜’
(╯°□°)╯︵ ┻━┻)
(ノಥ益ಥ)ノ ┻━┻
┬─┬ノ( º _ ºノ)
( ͡° ͜ʖ ͡°)
¯\_(ツ)_/¯

#	Emoji
#
#	Strings which contain Emoji; should be the same behavior as two-byte characters, but not always

😍
👩🏽
👨‍🦰 👨🏿‍🦰 👨‍🦱 👨🏿‍🦱 🦹🏿‍♂️
👾 🙇 💁 🙅 🙆 🙋 🙎 🙍
🐵 🙈 🙉 🙊
❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙
✋🏿 💪🏿 👐🏿 🙌🏿 👏🏿 🙏🏿
👨‍👩‍👦 👨‍👩‍👧‍👦 👨‍👨‍👦 👩‍👩‍👧 👨‍👦 👨‍👧‍👦 👩‍👦 👩‍👧‍👦
🚾 🆒 🆓 🆕 🆖 🆗 🆙 🏧
0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟

#       Regional Indicator Symbols
#
#       Regional Indicator Symbols can be displayed differently across
#       fonts, and have a number of special behaviors

🇺🇸🇷🇺🇸 🇦🇫🇦🇲🇸
🇺🇸🇷🇺🇸🇦🇫🇦🇲
🇺🇸🇷🇺🇸🇦

#	Unicode Numbers
#
#	Strings which contain unicode numbers; if the code is localized, it should see the input as numeric

123
١٢٣

#	Right-To-Left Strings
#
#	Strings which contain text that should be rendered RTL if possible (e.g. Arabic, Hebrew)

ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو.
בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ
הָיְתָהtestالصفحات التّحول


مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،
الكل في المجمو عة (5)

#	Ogham Text
#
#	The only unicode alphabet to use a space which isn't empty but should still act like a space.

᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜
᚛                 ᚜

#	Trick Unicode
#
#	Strings which contain unicode with unusual properties (e.g. Right-to-left override) (c.f. http://www.unicode.org/charts/PDF/U2000.pdf)

‪‪test‪
‫test‫

test

test⁠test‫
⁦test⁧

#	Zalgo Text
#
#	Strings which contain "corrupted" text. The corruption will not appear in non-HTML text, however. (via http://www.eeemo.net)

Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣
̡͓̞ͅI̗̘̦͝n͇͇͙v̮̫ok̲̫̙͈i̖͙̭̹̠̞n̡̻̮̣̺g̲͈͙̭͙̬͎ ̰t͔̦h̞̲e̢̤ ͍̬̲͖f̴̘͕̣è͖ẹ̥̩l͖͔͚i͓͚̦͠n͖͍̗͓̳̮g͍ ̨o͚̪͡f̘̣̬ ̖̘͖̟͙̮c҉͔̫͖͓͇͖ͅh̵̤̣͚͔á̗̼͕ͅo̼̣̥s̱͈̺̖̦̻͢.̛̖̞̠̫̰
̗̺͖̹̯͓Ṯ̤͍̥͇͈h̲́e͏͓̼̗̙̼̣͔ ͇̜̱̠͓͍ͅN͕͠e̗̱z̘̝̜̺͙p̤̺̹͍̯͚e̠̻̠͜r̨̤͍̺̖͔̖̖d̠̟̭̬̝͟i̦͖̩͓͔̤a̠̗̬͉̙n͚͜ ̻̞̰͚ͅh̵͉i̳̞v̢͇ḙ͎͟-҉̭̩̼͔m̤̭̫i͕͇̝̦n̗͙ḍ̟ ̯̲͕͞ǫ̟̯̰̲͙̻̝f ̪̰̰̗̖̭̘͘c̦͍̲̞͍̩̙ḥ͚a̮͎̟̙͜ơ̩̹͎s̤.̝̝ ҉Z̡̖̜͖̰̣͉̜a͖̰͙̬͡l̲̫̳͍̩g̡̟̼̱͚̞̬ͅo̗͜.̟
̦H̬̤̗̤͝e͜ ̜̥̝̻͍̟́w̕h̖̯͓o̝͙̖͎̱̮ ҉̺̙̞̟͈W̷̼̭a̺̪͍į͈͕̭͙̯̜t̶̼̮s̘͙͖̕ ̠̫̠B̻͍͙͉̳ͅe̵h̵̬͇̫͙i̹͓̳̳̮͎̫̕n͟d̴̪̜̖ ̰͉̩͇͙̲͞ͅT͖̼͓̪͢h͏͓̮̻e̬̝̟ͅ ̤̹̝W͙̞̝͔͇͝ͅa͏͓͔̹̼̣l̴͔̰̤̟͔ḽ̫.͕
Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮

#	Unicode Upsidedown
#
#	Strings which contain unicode with an "upsidedown" effect (via http://www.upsidedowntext.com)

˙ɐnbᴉlɐ ɐuƃɐɯ ǝɹolop ʇǝ ǝɹoqɐl ʇn ʇunpᴉpᴉɔuᴉ ɹodɯǝʇ poɯsnᴉǝ op pǝs 'ʇᴉlǝ ƃuᴉɔsᴉdᴉpɐ ɹnʇǝʇɔǝsuoɔ 'ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥
00˙Ɩ$-

#	Unicode font
#
#	Strings which contain bold/italic/etc. versions of normal characters

The quick brown fox jumps over the lazy dog
𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠
𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌
𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈
𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰
𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘
𝚃𝚑𝚎 𝚚𝚞𝚒𝚌𝚔 𝚋𝚛𝚘𝚠𝚗 𝚏𝚘𝚡 𝚓𝚞𝚖𝚙𝚜 𝚘𝚟𝚎𝚛 𝚝𝚑𝚎 𝚕𝚊𝚣𝚢 𝚍𝚘𝚐
⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢

#	Script Injection
#
#	Strings which attempt to invoke a benign script injection; shows vulnerability to XSS

<script>alert(0)</script>
&lt;script&gt;alert(&#39;1&#39;);&lt;/script&gt;
<img src=x onerror=alert(2) />
<svg><script>123<1>alert(3)</script>
"><script>alert(4)</script>
'><script>alert(5)</script>
><script>alert(6)</script>
</script><script>alert(7)</script>
< / script >< script >alert(8)< / script >
 onfocus=JaVaSCript:alert(9) autofocus
" onfocus=JaVaSCript:alert(10) autofocus
' onfocus=JaVaSCript:alert(11) autofocus
<script>alert(12)</script>
<sc<script>ript>alert(13)</sc</script>ript>
--><script>alert(14)</script>
";alert(15);t="
';alert(16);t='
JavaSCript:alert(17)
;alert(18);
src=JaVaSCript:prompt(19)
"><script>alert(20);</script x="
'><script>alert(21);</script x='
><script>alert(22);</script x=
" autofocus onkeyup="javascript:alert(23)
' autofocus onkeyup='javascript:alert(24)
<script\x20type="text/javascript">javascript:alert(25);</script>
<script\x3Etype="text/javascript">javascript:alert(26);</script>
<script\x0Dtype="text/javascript">javascript:alert(27);</script>
<script\x09type="text/javascript">javascript:alert(28);</script>
<script\x0Ctype="text/javascript">javascript:alert(29);</script>
<script\x2Ftype="text/javascript">javascript:alert(30);</script>
<script\x0Atype="text/javascript">javascript:alert(31);</script>
'`"><\x3Cscript>javascript:alert(32)</script>
'`"><\x00script>javascript:alert(33)</script>
ABC<div style="x\x3Aexpression(javascript:alert(34)">DEF
ABC<div style="x:expression\x5C(javascript:alert(35)">DEF
ABC<div style="x:expression\x00(javascript:alert(36)">DEF
ABC<div style="x:exp\x00ression(javascript:alert(37)">DEF
ABC<div style="x:exp\x5Cression(javascript:alert(38)">DEF
ABC<div style="x:\x0Aexpression(javascript:alert(39)">DEF
ABC<div style="x:\x09expression(javascript:alert(40)">DEF
ABC<div style="x:\xE3\x80\x80expression(javascript:alert(41)">DEF
ABC<div style="x:\xE2\x80\x84expression(javascript:alert(42)">DEF
ABC<div style="x:\xC2\xA0expression(javascript:alert(43)">DEF
ABC<div style="x:\xE2\x80\x80expression(javascript:alert(44)">DEF
ABC<div style="x:\xE2\x80\x8Aexpression(javascript:alert(45)">DEF
ABC<div style="x:\x0Dexpression(javascript:alert(46)">DEF
ABC<div style="x:\x0Cexpression(javascript:alert(47)">DEF
ABC<div style="x:\xE2\x80\x87expression(javascript:alert(48)">DEF
ABC<div style="x:\xEF\xBB\xBFexpression(javascript:alert(49)">DEF
ABC<div style="x:\x20expression(javascript:alert(50)">DEF
ABC<div style="x:\xE2\x80\x88expression(javascript:alert(51)">DEF
ABC<div style="x:\x00expression(javascript:alert(52)">DEF
ABC<div style="x:\xE2\x80\x8Bexpression(javascript:alert(53)">DEF
ABC<div style="x:\xE2\x80\x86expression(javascript:alert(54)">DEF
ABC<div style="x:\xE2\x80\x85expression(javascript:alert(55)">DEF
ABC<div style="x:\xE2\x80\x82expression(javascript:alert(56)">DEF
ABC<div style="x:\x0Bexpression(javascript:alert(57)">DEF
ABC<div style="x:\xE2\x80\x81expression(javascript:alert(58)">DEF
ABC<div style="x:\xE2\x80\x83expression(javascript:alert(59)">DEF
ABC<div style="x:\xE2\x80\x89expression(javascript:alert(60)">DEF
<a href="\x0Bjavascript:javascript:alert(61)" id="fuzzelement1">test</a>
<a href="\x0Fjavascript:javascript:alert(62)" id="fuzzelement1">test</a>
<a href="\xC2\xA0javascript:javascript:alert(63)" id="fuzzelement1">test</a>
<a href="\x05javascript:javascript:alert(64)" id="fuzzelement1">test</a>
<a href="\xE1\xA0\x8Ejavascript:javascript:alert(65)" id="fuzzelement1">test</a>
<a href="\x18javascript:javascript:alert(66)" id="fuzzelement1">test</a>
<a href="\x11javascript:javascript:alert(67)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x88javascript:javascript:alert(68)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x89javascript:javascript:alert(69)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x80javascript:javascript:alert(70)" id="fuzzelement1">test</a>
<a href="\x17javascript:javascript:alert(71)" id="fuzzelement1">test</a>
<a href="\x03javascript:javascript:alert(72)" id="fuzzelement1">test</a>
<a href="\x0Ejavascript:javascript:alert(73)" id="fuzzelement1">test</a>
<a href="\x1Ajavascript:javascript:alert(74)" id="fuzzelement1">test</a>
<a href="\x00javascript:javascript:alert(75)" id="fuzzelement1">test</a>
<a href="\x10javascript:javascript:alert(76)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x82javascript:javascript:alert(77)" id="fuzzelement1">test</a>
<a href="\x20javascript:javascript:alert(78)" id="fuzzelement1">test</a>
<a href="\x13javascript:javascript:alert(79)" id="fuzzelement1">test</a>
<a href="\x09javascript:javascript:alert(80)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x8Ajavascript:javascript:alert(81)" id="fuzzelement1">test</a>
<a href="\x14javascript:javascript:alert(82)" id="fuzzelement1">test</a>
<a href="\x19javascript:javascript:alert(83)" id="fuzzelement1">test</a>
<a href="\xE2\x80\xAFjavascript:javascript:alert(84)" id="fuzzelement1">test</a>
<a href="\x1Fjavascript:javascript:alert(85)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x81javascript:javascript:alert(86)" id="fuzzelement1">test</a>
<a href="\x1Djavascript:javascript:alert(87)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x87javascript:javascript:alert(88)" id="fuzzelement1">test</a>
<a href="\x07javascript:javascript:alert(89)" id="fuzzelement1">test</a>
<a href="\xE1\x9A\x80javascript:javascript:alert(90)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x83javascript:javascript:alert(91)" id="fuzzelement1">test</a>
<a href="\x04javascript:javascript:alert(92)" id="fuzzelement1">test</a>
<a href="\x01javascript:javascript:alert(93)" id="fuzzelement1">test</a>
<a href="\x08javascript:javascript:alert(94)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x84javascript:javascript:alert(95)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x86javascript:javascript:alert(96)" id="fuzzelement1">test</a>
<a href="\xE3\x80\x80javascript:javascript:alert(97)" id="fuzzelement1">test</a>
<a href="\x12javascript:javascript:alert(98)" id="fuzzelement1">test</a>
<a href="\x0Djavascript:javascript:alert(99)" id="fuzzelement1">test</a>
<a href="\x0Ajavascript:javascript:alert(100)" id="fuzzelement1">test</a>
<a href="\x0Cjavascript:javascript:alert(101)" id="fuzzelement1">test</a>
<a href="\x15javascript:javascript:alert(102)" id="fuzzelement1">test</a>
<a href="\xE2\x80\xA8javascript:javascript:alert(103)" id="fuzzelement1">test</a>
<a href="\x16javascript:javascript:alert(104)" id="fuzzelement1">test</a>
<a href="\x02javascript:javascript:alert(105)" id="fuzzelement1">test</a>
<a href="\x1Bjavascript:javascript:alert(106)" id="fuzzelement1">test</a>
<a href="\x06javascript:javascript:alert(107)" id="fuzzelement1">test</a>
<a href="\xE2\x80\xA9javascript:javascript:alert(108)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x85javascript:javascript:alert(109)" id="fuzzelement1">test</a>
<a href="\x1Ejavascript:javascript:alert(110)" id="fuzzelement1">test</a>
<a href="\xE2\x81\x9Fjavascript:javascript:alert(111)" id="fuzzelement1">test</a>
<a href="\x1Cjavascript:javascript:alert(112)" id="fuzzelement1">test</a>
<a href="javascript\x00:javascript:alert(113)" id="fuzzelement1">test</a>
<a href="javascript\x3A:javascript:alert(114)" id="fuzzelement1">test</a>
<a href="javascript\x09:javascript:alert(115)" id="fuzzelement1">test</a>
<a href="javascript\x0D:javascript:alert(116)" id="fuzzelement1">test</a>
<a href="javascript\x0A:javascript:alert(117)" id="fuzzelement1">test</a>
`"'><img src=xxx:x \x0Aonerror=javascript:alert(118)>
`"'><img src=xxx:x \x22onerror=javascript:alert(119)>
`"'><img src=xxx:x \x0Bonerror=javascript:alert(120)>
`"'><img src=xxx:x \x0Donerror=javascript:alert(121)>
`"'><img src=xxx:x \x2Fonerror=javascript:alert(122)>
`"'><img src=xxx:x \x09onerror=javascript:alert(123)>
`"'><img src=xxx:x \x0Conerror=javascript:alert(124)>
`"'><img src=xxx:x \x00onerror=javascript:alert(125)>
`"'><img src=xxx:x \x27onerror=javascript:alert(126)>
`"'><img src=xxx:x \x20onerror=javascript:alert(127)>
"`'><script>\x3Bjavascript:alert(128)</script>
"`'><script>\x0Djavascript:alert(129)</script>
"`'><script>\xEF\xBB\xBFjavascript:alert(130)</script>
"`'><script>\xE2\x80\x81javascript:alert(131)</script>
"`'><script>\xE2\x80\x84javascript:alert(132)</script>
"`'><script>\xE3\x80\x80javascript:alert(133)</script>
"`'><script>\x09javascript:alert(134)</script>
"`'><script>\xE2\x80\x89javascript:alert(135)</script>
"`'><script>\xE2\x80\x85javascript:alert(136)</script>
"`'><script>\xE2\x80\x88javascript:alert(137)</script>
"`'><script>\x00javascript:alert(138)</script>
"`'><script>\xE2\x80\xA8javascript:alert(139)</script>
"`'><script>\xE2\x80\x8Ajavascript:alert(140)</script>
"`'><script>\xE1\x9A\x80javascript:alert(141)</script>
"`'><script>\x0Cjavascript:alert(142)</script>
"`'><script>\x2Bjavascript:alert(143)</script>
"`'><script>\xF0\x90\x96\x9Ajavascript:alert(144)</script>
"`'><script>-javascript:alert(145)</script>
"`'><script>\x0Ajavascript:alert(146)</script>
"`'><script>\xE2\x80\xAFjavascript:alert(147)</script>
"`'><script>\x7Ejavascript:alert(148)</script>
"`'><script>\xE2\x80\x87javascript:alert(149)</script>
"`'><script>\xE2\x81\x9Fjavascript:alert(150)</script>
"`'><script>\xE2\x80\xA9javascript:alert(151)</script>
"`'><script>\xC2\x85javascript:alert(152)</script>
"`'><script>\xEF\xBF\xAEjavascript:alert(153)</script>
"`'><script>\xE2\x80\x83javascript:alert(154)</script>
"`'><script>\xE2\x80\x8Bjavascript:alert(155)</script>
"`'><script>\xEF\xBF\xBEjavascript:alert(156)</script>
"`'><script>\xE2\x80\x80javascript:alert(157)</script>
"`'><script>\x21javascript:alert(158)</script>
"`'><script>\xE2\x80\x82javascript:alert(159)</script>
"`'><script>\xE2\x80\x86javascript:alert(160)</script>
"`'><script>\xE1\xA0\x8Ejavascript:alert(161)</script>
"`'><script>\x0Bjavascript:alert(162)</script>
"`'><script>\x20javascript:alert(163)</script>
"`'><script>\xC2\xA0javascript:alert(164)</script>
<img \x00src=x onerror="alert(165)">
<img \x47src=x onerror="javascript:alert(166)">
<img \x11src=x onerror="javascript:alert(167)">
<img \x12src=x onerror="javascript:alert(168)">
<img\x47src=x onerror="javascript:alert(169)">
<img\x10src=x onerror="javascript:alert(170)">
<img\x13src=x onerror="javascript:alert(171)">
<img\x32src=x onerror="javascript:alert(172)">
<img\x47src=x onerror="javascript:alert(173)">
<img\x11src=x onerror="javascript:alert(174)">
<img \x47src=x onerror="javascript:alert(175)">
<img \x34src=x onerror="javascript:alert(176)">
<img \x39src=x onerror="javascript:alert(177)">
<img \x00src=x onerror="javascript:alert(178)">
<img src\x09=x onerror="javascript:alert(179)">
<img src\x10=x onerror="javascript:alert(180)">
<img src\x13=x onerror="javascript:alert(181)">
<img src\x32=x onerror="javascript:alert(182)">
<img src\x12=x onerror="javascript:alert(183)">
<img src\x11=x onerror="javascript:alert(184)">
<img src\x00=x onerror="javascript:alert(185)">
<img src\x47=x onerror="javascript:alert(186)">
<img src=x\x09onerror="javascript:alert(187)">
<img src=x\x10onerror="javascript:alert(188)">
<img src=x\x11onerror="javascript:alert(189)">
<img src=x\x12onerror="javascript:alert(190)">
<img src=x\x13onerror="javascript:alert(191)">
<img[a][b][c]src[d]=x[e]onerror=[f]"alert(192)">
<img src=x onerror=\x09"javascript:alert(193)">
<img src=x onerror=\x10"javascript:alert(194)">
<img src=x onerror=\x11"javascript:alert(195)">
<img src=x onerror=\x12"javascript:alert(196)">
<img src=x onerror=\x32"javascript:alert(197)">
<img src=x onerror=\x00"javascript:alert(198)">
<a href=java&#1&#2&#3&#4&#5&#6&#7&#8&#11&#12script:javascript:alert(199)>XXX</a>
<img src="x` `<script>javascript:alert(200)</script>"` `>
<img src onerror /" '"= alt=javascript:alert(201)//">
<title onpropertychange=javascript:alert(202)></title><title title=>
<a href=http://foo.bar/#x=`y></a><img alt="`><img src=x:x onerror=javascript:alert(203)></a>">
<!--[if]><script>javascript:alert(204)</script -->
<!--[if<img src=x onerror=javascript:alert(205)//]> -->
<script src="/\%(jscript)s"></script>
<script src="\\%(jscript)s"></script>
<IMG """><SCRIPT>alert("206")</SCRIPT>">
<IMG SRC=javascript:alert(String.fromCharCode(50,48,55))>
<IMG SRC=# onmouseover="alert('208')">
<IMG SRC= onmouseover="alert('209')">
<IMG onmouseover="alert('210')">
<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#50;&#49;&#49;&#39;&#41;>
<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000050&#0000049&#0000050&#0000039&#0000041>
<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x32&#x31&#x33&#x27&#x29>
<IMG SRC="jav   ascript:alert('214');">
<IMG SRC="jav&#x09;ascript:alert('215');">
<IMG SRC="jav&#x0A;ascript:alert('216');">
<IMG SRC="jav&#x0D;ascript:alert('217');">
perl -e 'print "<IMG SRC=java\0script:alert(\"218\")>";' > out
<IMG SRC=" &#14;  javascript:alert('219');">
<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>
<BODY onload!#$%&()*~+-_.,:;?@[/|\]^`=alert("220")>
<SCRIPT/SRC="http://ha.ckers.org/xss.js"></SCRIPT>
<<SCRIPT>alert("221");//<</SCRIPT>
<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >
<SCRIPT SRC=//ha.ckers.org/.j>
<IMG SRC="javascript:alert('222')"
<iframe src=http://ha.ckers.org/scriptlet.html <
\";alert('223');//
<u oncopy=alert()> Copy me</u>
<i onwheel=alert(224)> Scroll over me </i>
<plaintext>
http://a/%%30%30
</textarea><script>alert(225)</script>

#	SQL Injection
#
#	Strings which can cause a SQL injection if inputs are not sanitized

1;DROP TABLE users
1'; DROP TABLE users-- 1
' OR 1=1 -- 1
' OR '1'='1
'; EXEC sp_MSForEachTable 'DROP TABLE ?'; --
 
%
_

#	Server Code Injection
#
#	Strings which can cause user to run code on server as a privileged user (c.f. https://news.ycombinator.com/item?id=7665153)

-
--
--version
--help
$USER
/dev/null; touch /tmp/blns.fail ; echo
`touch /tmp/blns.fail`
$(touch /tmp/blns.fail)
@{[system "touch /tmp/blns.fail"]}

#	Command Injection (Ruby)
#
#	Strings which can call system commands within Ruby/Rails applications

eval("puts 'hello world'")
System("ls -al /")
`ls -al /`
Kernel.exec("ls -al /")
Kernel.exit(1)
%x('ls -al /')

#      XXE Injection (XML)
#
#	String which can reveal system files when parsed by a badly configured XML parser

<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>

#	Unwanted Interpolation
#
#	Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string.

$HOME
$ENV{'HOME'}
%d
%s%s%s%s%s
{0}
%*.*s
%@
%n
File:///

#	File Inclusion
#
#	Strings which can cause user to pull in files that should not be a part of a web server

../../../../../../../../../../../etc/passwd%00
../../../../../../../../../../../etc/hosts

#	Known CVEs and Vulnerabilities
#
#	Strings that test for known vulnerabilities

() { 0; }; touch /tmp/blns.shellshock1.fail;
() { _; } >_[$($())] { touch /tmp/blns.shellshock2.fail; }
<<< %s(un='%s') = %u
+++ATH0

#	MSDOS/Windows Special Filenames
#
#	Strings which are reserved characters in MSDOS/Windows

CON
PRN
AUX
CLOCK$
NUL
A:
ZZ:
COM1
LPT1
LPT2
LPT3
COM2
COM3
COM4

#   IRC specific strings
#
#   Strings that may occur on IRC clients that make security products freak out

DCC SEND STARTKEYLOGGER 0 0 0

#	Scunthorpe Problem
#
#	Innocuous strings which may be blocked by profanity filters (https://en.wikipedia.org/wiki/Scunthorpe_problem)

Scunthorpe General Hospital
Penistone Community Church
Lightwater Country Park
Jimmy Clitheroe
Horniman Museum
shitake mushrooms
RomansInSussex.co.uk
http://www.cum.qc.ca/
Craig Cockburn, Software Specialist
Linda Callahan
Dr. Herman I. Libshitz
magna cum laude
Super Bowl XXX
medieval erection of parapets
evaluate
mocha
expression
Arsenal canal
classic
Tyson Gay
Dick Van Dyke
basement

#	Human injection
#
#	Strings which may cause human to reinterpret worldview

If you're reading this, you've been in a coma for almost 20 years now. We're trying a new technique. We don't know where this message will end up in your dream, but we hope it works. Please wake up, we miss you.

#	Terminal escape codes
#
#	Strings which punish the fools who use cat/type on this file

Roses are red, violets are blue. Hope you enjoy terminal hue
But now...for my greatest trick...
The quick brown fox... [Beeeep]

#	iOS Vulnerabilities
#
#	Strings which crashed iMessage in various versions of iOS

Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗
🏳0🌈️
జ్ఞ‌ా

# Persian special characters
#
# This is a four characters string which includes Persian special characters (گچپژ)

گچپژ

# jinja2 injection
#
# first one is supposed to raise "MemoryError" exception
# second, obviously, prints contents of /etc/passwd

{% print 'x' * 64 * 1024**3 %}
{{ "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() }}

Added testdata/testbox/00000000000100.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
expert-mode: true
modified: 20220215171142
visibility: owner

Added testdata/testbox/19700101000000.zettel.













1
2
3
4
5
6
7
8
9
10
11
12
+
+
+
+
+
+
+
+
+
+
+
+
id: 19700101000000
title: Startup Configuration
role: configuration
tags: #invisible
syntax: none
box-uri-1: mem:
box-uri-2: dir:testdata/testbox?readonly
modified: 20210629174022
owner: 20210629163300
secret: 1234567890123456
token-lifetime-api: 1
visibility: owner

Added testdata/testbox/20210629163300.zettel.










1
2
3
4
5
6
7
8
9
+
+
+
+
+
+
+
+
+
id: 20210629163300
title: Owena
role: user
tags: #test #user
syntax: none
credential: $2a$10$gcKyVmQ50fwgpOjyiiCm4eba/ILrNXoxTUCopgTEnYTa4yuceHMC6
modified: 20211220131749
user-id: owner

Added testdata/testbox/20210629165000.zettel.











1
2
3
4
5
6
7
8
9
10
+
+
+
+
+
+
+
+
+
+
id: 20210629165000
title: Woody
role: user
tags: #test #user
syntax: none
credential: $2a$10$VmHPyXa0Bm8DE4MJ.pQnbuuQmweWtyGya0L/bFA4nIuCn1EvPQflK
modified: 20211220132007
user-id: writer
user-role: writer

Added testdata/testbox/20210629165024.zettel.











1
2
3
4
5
6
7
8
9
10
+
+
+
+
+
+
+
+
+
+
id: 20210629165024
title: Reanna
role: user
tags: #test #user
syntax: none
credential: $2a$10$uC7LV2JdFhasw2HqSWZbSOihvFpwtaEXjXp98yzGfE3FHudq.vg.u
modified: 20211220131906
user-id: reader
user-role: reader

Added testdata/testbox/20210629165050.zettel.











1
2
3
4
5
6
7
8
9
10
+
+
+
+
+
+
+
+
+
+
id: 20210629165050
title: Creighton
role: user
tags: #test #user
syntax: none
credential: $2a$10$z85253tqhbHlXPZpt0hJpughLR4WXY8iYJbm1LlBhrKsL1YfkRy2q
modified: 20211220131520
user-id: creator
user-role: creator

Added testdata/testbox/20211019200500.zettel.











1
2
3
4
5
6
7
8
9
10
+
+
+
+
+
+
+
+
+
+
id: 20211019200500
title: Collection of all users
role: zettel
syntax: zmk
modified: 20211220132017

* [[Owena|20210629163300]]
* [[Woody|20210629165000]]
* [[Reanna|20210629165024]]
* [[Creighton|20210629165050]]

Added testdata/testbox/20211020121000.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
id: 20211020121000
title: ABC
role: zettel
syntax: zmk
modified: 20211020191331
visibility: owner

Ab C

Added testdata/testbox/20211020121100.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121100
title: 10*ABC
role: zettel
syntax: zmk

{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}

Added testdata/testbox/20211020121145.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121145
title: 10*10*ABC
role: zettel
syntax: zmk

{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}

Added testdata/testbox/20211020121300.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121300
title: 10*10*10*ABC
role: zettel
syntax: zmk

{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}

Added testdata/testbox/20211020121400.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121400
title: 10*10*10*10*ABC
role: zettel
syntax: zmk

{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}

Added testdata/testbox/20211020182600.zettel.








1
2
3
4
5
6
7
+
+
+
+
+
+
+
id: 20211020182600
title: Self-recursive transclusion
role: zettel
syntax: zmk
modified: 20211020182712

{{20211020182600}}

Added testdata/testbox/20211020183700.zettel.








1
2
3
4
5
6
7
+
+
+
+
+
+
+
id: 20211020183700
title: Indirect Recursive 1
role: zettel
syntax: zmk
modified: 20211020183932

{{20211020183800}}

Added testdata/testbox/20211020183800.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020183800
title: Indirect Recursive 2
role: zettel
syntax: zmk

{{20211020183700}}

Added testdata/testbox/20211020184300.zettel.






1
2
3
4
5
+
+
+
+
+
id: 20211020184300
title: Empty Zettel
role: zettel
syntax: zmk

Added testdata/testbox/20211020184342.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020184342
title: Transclude empty Zettel
role: zettel
syntax: zmk

{{20211020184300}}

Added testdata/testbox/20211020185400.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
id: 20211020185400
title: Self embed zettel
role: zettel
syntax: zmk
modified: 20211119150218

{{#mark}}
[!mark] Home

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.

Added 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/client.fossil/client"
	"zettelstore.de/z/kernel"
)

func nextZid(zid api.ZettelID) api.ZettelID {
	numVal, err := strconv.ParseUint(string(zid), 10, 64)
	if err != nil {
		panic(err)
	}
	return api.ZettelID(fmt.Sprintf("%014d", numVal+1))
}

func TestNextZid(t *testing.T) {
	testCases := []struct {
		zid, exp api.ZettelID
	}{
		{api.ZettelID("00000000000000"), api.ZettelID("00000000000001")},
	}
	for i, tc := range testCases {
		if got := nextZid(tc.zid); got != tc.exp {
			t.Errorf("%d: zid=%q, exp=%q, got=%q", i, tc.zid, tc.exp, got)
		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 56
		configRoleZettel = 34
		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.QueryZettelData(context.Background(), "")
			if err != nil {
				tt.Error(err)
				return
			}
			if q != "" {
				tt.Errorf("Query should be empty, but is %q", q)
			}
			if h != "" {
				tt.Errorf("Human should be empty, but is %q", q)
			}
			got := len(l)
			if got != tc.exp {
				tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
			}
		})
	}
	search := api.KeyRole + api.SearchOperatorHas + api.ValueRoleConfiguration + " ORDER id"
	q, h, l, err := c.QueryZettelData(context.Background(), search)
	if err != nil {
		t.Error(err)
		return
	}
	expQ := "role:configuration ORDER id"
	if q != expQ {
		t.Errorf("Query should be %q, but is %q", expQ, q)
	}
	expH := "role HAS configuration ORDER id"
	if h != expH {
		t.Errorf("Human should be %q, but is %q", expH, h)
	}
	got := len(l)
	if got != configRoleZettel {
		t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l)
	}

	pl, err := c.QueryZettel(context.Background(), search)
	if err != nil {
		t.Error(err)
		return
	}
	compareZettelList(t, pl, l)
}

func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) {
	t.Helper()
	if len(pl) != len(l) {
		t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l))
	} else {
		for i, line := range pl {
			if got := api.ZettelID(line[:14]); got != l[i].ID {
				t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got)
			}
		}
	}
}

func TestGetZettelData(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if m := z.Meta; len(m) == 0 {
		t.Errorf("Exptected non-empty meta, but got %v", z.Meta)
	}
	if z.Content == "" || z.Encoding != "" {
		t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding)
	}

	mr, err := c.GetMetaData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if mr.Rights == api.ZettelCanNone {
		t.Error("rights must be greater zero")
	}
	if len(mr.Meta) != len(z.Meta) {
		t.Errorf("Pure meta differs from zettel meta: %s vs %s", mr.Meta, z.Meta)
		return
	}
	for k, v := range z.Meta {
		got, ok := mr.Meta[k]
		if !ok {
			t.Errorf("Pure meta has no key %q", k)
			continue
		}
		if got != v {
			t.Errorf("Pure meta has different value for key %q: %q vs %q", k, got, v)
		}
	}
}

func TestGetParsedEvaluatedZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	encodings := []api.EncodingEnum{
		api.EncoderHTML,
		api.EncoderSz,
		api.EncoderText,
	}
	for _, enc := range encodings {
		content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for parsed encoding %v", enc)
		}
		content, err = c.GetEvaluatedZettel(context.Background(), api.ZidDefaultHome, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for evaluated encoding %v", enc)
		}
	}
}

func checkListZid(t *testing.T, l []api.ZidMetaRights, pos int, expected api.ZettelID) {
	t.Helper()
	if got := api.ZettelID(l[pos].ID); got != expected {
		t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got)
	}
}

func TestGetZettelOrder(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if got := len(metaSeq); got != 4 {
		t.Errorf("Expected list of length 4, got %d", got)
		return
	}
	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)
// 	if err != nil {
// 		t.Error(err)
// 		return
// 	}
// 	if !checkZid(t, ownerZid, rl.ID) {
// 		return
// 	}
// 	l := rl.List
// 	if got := len(l); got != limitAll {
// 		t.Errorf("Expected list of length %d, got %d", limitAll, got)
// 		t.Error(rl)
// 		return
// 	}
// 	checkListZid(t, l, 0, allUserZid)
// 	// checkListZid(t, l, 1, writerZid)
// 	// checkListZid(t, l, 2, readerZid)
// 	checkListZid(t, l, 1, creatorZid)

// 	rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0)
// 	if err != nil {
// 		t.Error(err)
// 		return
// 	}
// 	if !checkZid(t, ownerZid, rl.ID) {
// 		return
// 	}
// 	l = rl.List
// 	if got, exp := len(l), 4; got != exp {
// 		t.Errorf("Expected list of length %d, got %d", exp, got)
// 		return
// 	}
// 	checkListZid(t, l, 0, allUserZid)
// }

func TestGetUnlinkedReferences(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if got := len(metaSeq); got != 1 {
		t.Errorf("Expected list of length 1, got %d:\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 {
			if cErr.StatusCode == goodCode {
				return false
			}
			t.Errorf("Expect status code %d, but got client error %v", goodCode, cErr)
		} else {
			t.Errorf("Expect status code %d, but got non-client error %v", goodCode, err)
		}
	} else {
		t.Errorf("No error returned, but status code %d expected", goodCode)
	}
	return true
}

func TestExecuteCommand(t *testing.T) {
	c := getClient()
	err := c.ExecuteCommand(context.Background(), api.Command("xyz"))
	failNoErrorOrNoCode(t, err, http.StatusBadRequest)
	err = c.ExecuteCommand(context.Background(), api.CommandAuthenticated)
	failNoErrorOrNoCode(t, err, http.StatusUnauthorized)
	err = c.ExecuteCommand(context.Background(), api.CommandRefresh)
	failNoErrorOrNoCode(t, err, http.StatusForbidden)

	c.SetAuth("owner", "owner")
	err = c.ExecuteCommand(context.Background(), api.CommandAuthenticated)
	if err != nil {
		t.Error(err)
	}
	err = c.ExecuteCommand(context.Background(), api.CommandRefresh)
	if err != nil {
		t.Error(err)
	}
}

func TestListTags(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyTags)
	if err != nil {
		t.Error(err)
		return
	}
	tags := []struct {
		key  string
		size int
	}{
		{"#invisible", 1},
		{"#user", 4},
		{"#test", 4},
	}
	if len(agg) != len(tags) {
		t.Errorf("Expected %d different tags, but got %d (%v)", len(tags), len(agg), agg)
	}
	for _, tag := range tags {
		if zl, ok := agg[tag.key]; !ok {
			t.Errorf("No tag %v: %v", tag.key, agg)
		} else if len(zl) != tag.size {
			t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
		}
	}
	for i, id := range agg["#user"] {
		if id != agg["#test"][i] {
			t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"])
		}
	}
}

func 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")
	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
	}
	if !slices.Equal(bodyRedirect, bodyEmoji) {
		t.Error("Wrong redirect")
		t.Error("REDIRECT", respRedirect)
		t.Error("EXPECTED", respEmoji)
	}
}

func TestVersion(t *testing.T) {
	t.Parallel()
	c := getClient()
	ver, err := c.GetVersionInfo(context.Background())
	if err != nil {
		t.Error(err)
		return
	}
	if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" {
		t.Error(ver)
	}
}

var baseURL string

func init() {
	flag.StringVar(&baseURL, "base-url", "", "Base URL")
}

func getClient() *client.Client {
	u, err := url.Parse(baseURL)
	if err != nil {
		panic(err)
	}
	return client.NewClient(u)
}

// TestMain controls whether client API tests should run or not.
func TestMain(m *testing.M) {
	flag.Parse()
	if baseURL != "" {
		m.Run()
	}
}

Added 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/client.fossil/client"
)

// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.

func TestCreateGetRenameDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	zettel := `title: A Test

Example content.`
	c := getClient()
	c.SetAuth("owner", "owner")
	zid, err := c.CreateZettel(context.Background(), []byte(zettel))
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	if !zid.IsValid() {
		t.Error("Invalid zettel ID", zid)
		return
	}
	data, err := c.GetZettel(context.Background(), zid, api.PartZettel)
	if err != nil {
		t.Error("Cannot read zettel", zid, err)
		return
	}
	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)
}

func TestCreateGetRenameDeleteZettelData(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("creator", "creator")
	zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
		Meta:     nil,
		Encoding: "",
		Content:  "Example",
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	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)
}

func TestCreateGetDeleteZettelData(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("owner", "owner")
	wrongModified := "19691231115959"
	zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
		Meta: api.ZettelMeta{
			api.KeyTitle:    "A\nTitle", // \n must be converted into a space
			api.KeyModified: wrongModified,
		},
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	z, err := c.GetZettelData(context.Background(), zid)
	if err != nil {
		t.Error("Cannot get zettel:", zid, err)
	} else {
		exp := "A Title"
		if got := z.Meta[api.KeyTitle]; got != exp {
			t.Errorf("Expected title %q, but got %q", exp, got)
		}
		if got := z.Meta[api.KeyModified]; got != "" {
			t.Errorf("Create allowed to set the modified key: %q", got)
		}
	}
	doDelete(t, c, zid)
}

func TestUpdateZettel(t *testing.T) {
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel)
	if err != nil {
		t.Error(err)
		return
	}
	if !strings.HasPrefix(string(z), "title: Home\n") {
		t.Error("Got unexpected zettel", z)
		return
	}
	newZettel := `title: Empty Home
role: zettel
syntax: zmk

Empty`
	err = c.UpdateZettel(context.Background(), api.ZidDefaultHome, []byte(newZettel))
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel)
	if err != nil {
		t.Error(err)
		return
	}
	if string(zt) != newZettel {
		t.Errorf("Expected zettel %q, got %q", newZettel, zt)
	}
	// Must delete to clean up for next tests
	doDelete(t, c, api.ZidDefaultHome)
}

func TestUpdateZettelData(t *testing.T) {
	c := getClient()
	c.SetAuth("writer", "writer")
	z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if got := z.Meta[api.KeyTitle]; got != "Home" {
		t.Errorf("Title of zettel is not \"Home\", but %q", got)
		return
	}
	newTitle := "New Home"
	z.Meta[api.KeyTitle] = newTitle
	wrongModified := "19691231235959"
	z.Meta[api.KeyModified] = wrongModified
	err = c.UpdateZettelData(context.Background(), api.ZidDefaultHome, z)
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if got := zt.Meta[api.KeyTitle]; got != newTitle {
		t.Errorf("Title of zettel is not %q, but %q", newTitle, got)
	}
	if got := zt.Meta[api.KeyModified]; got == wrongModified {
		t.Errorf("Update did not change the modified key: %q", got)
	}

	// Must delete to clean up for next tests
	c.SetAuth("owner", "owner")
	doDelete(t, c, api.ZidDefaultHome)
}

func doDelete(t *testing.T, c *client.Client, zid api.ZettelID) {
	err := c.DeleteZettel(context.Background(), zid)
	if err != nil {
		t.Helper()
		t.Error("Cannot delete", zid, ":", err)
	}
}

Added 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 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/client.fossil/api"
)

const (
	abcZid   = api.ZettelID("20211020121000")
	abc10Zid = api.ZettelID("20211020121100")
)

func TestZettelTransclusion(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const abc10000Zid = api.ZettelID("20211020121400")
	contentMap := map[api.ZettelID]int{
		abcZid:                         1,
		abc10Zid:                       10,
		api.ZettelID("20211020121145"): 100,
		api.ZettelID("20211020121300"): 1000,
	}
	content, err := c.GetZettel(context.Background(), abcZid, api.PartContent)
	if err != nil {
		t.Error(err)
		return
	}
	baseContent := string(content)
	for zid, siz := range contentMap {
		content, err = c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML)
		if err != nil {
			t.Error(err)
			continue
		}
		sContent := string(content)
		prefix := "<p>"
		if !strings.HasPrefix(sContent, prefix) {
			t.Errorf("Content of zettel %q does not start with %q: %q", zid, prefix, stringHead(sContent))
			continue
		}
		suffix := "</p>"
		if !strings.HasSuffix(sContent, suffix) {
			t.Errorf("Content of zettel %q does not end with %q: %q", zid, suffix, stringTail(sContent))
			continue
		}
		got := sContent[len(prefix) : len(content)-len(suffix)]
		if expect := strings.Repeat(baseContent, siz); expect != got {
			t.Errorf("Unexpected content for zettel %q\nExpect: %q\nGot:    %q", zid, expect, got)
		}
	}

	content, err = c.GetEvaluatedZettel(context.Background(), abc10000Zid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	checkContentContains(t, abc10000Zid, string(content), "Too many transclusions")
}

func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("reader", "reader")

	zettelData, err := c.GetZettelData(context.Background(), api.ZidEmoji)
	if err != nil {
		t.Error(err)
		return
	}
	expectedEnc := "base64"
	if got := zettelData.Encoding; expectedEnc != got {
		t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got)
	}

	content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	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 {
		return s
	}
	return s[:maxLen-3] + "..."
}

func stringTail(s string) string {
	const maxLen = 40
	if len(s) <= maxLen {
		return s
	}
	return "..." + s[len(s)-maxLen-3:]
}

func TestRecursiveTransclusion(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const (
		selfRecursiveZid      = api.ZettelID("20211020182600")
		indirectRecursive1Zid = api.ZettelID("20211020183700")
		indirectRecursive2Zid = api.ZettelID("20211020183800")
	)
	recursiveZettel := map[api.ZettelID]api.ZettelID{
		selfRecursiveZid:      selfRecursiveZid,
		indirectRecursive1Zid: indirectRecursive2Zid,
		indirectRecursive2Zid: indirectRecursive1Zid,
	}
	for zid, errZid := range recursiveZettel {
		content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML)
		if err != nil {
			t.Error(err)
			continue
		}
		sContent := string(content)
		checkContentContains(t, zid, sContent, "Recursive transclusion")
		checkContentContains(t, zid, sContent, string(errZid))
	}
}
func TestNothingToTransclude(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const (
		transZid = api.ZettelID("20211020184342")
		emptyZid = api.ZettelID("20211020184300")
	)
	content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	sContent := string(content)
	checkContentContains(t, transZid, sContent, "<!-- Nothing to transclude")
	checkContentContains(t, transZid, sContent, string(emptyZid))
}

func TestSelfEmbedRef(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const selfEmbedZid = api.ZettelID("20211020185400")
	content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	checkContentContains(t, selfEmbedZid, string(content), "Self embed reference")
}

func checkContentContains(t *testing.T, zid api.ZettelID, content, expected string) {
	if !strings.Contains(content, expected) {
		t.Helper()
		t.Errorf("Zettel %q should contain %q, but does not: %q", zid, expected, content)
	}
}

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
58
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





















1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29





30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49




























50
51
52


53
54
55

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74




75
76


77
78



79
80





81
82
83
84
85
86
87
88
89



90
91
92
93
94
95
96
97




























98
99
100
101
102
103


104
105

106
107



108
109
110
111

112
113
114
115
116
117



118
119
120
121

122
123
124
125
126


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

-
+

-
+




+
+
+


-



+



-



+
+

+


-
-
-
-
-
+
+
+
+
+



+











-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+

-
-
+
+

-
+









+








-
-
-
-
+

-
-
+
+
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+


-
-
-
+
+
+





-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+


+
-
-
+
+
-


-
-
-
+
+
+
+
-






-
-
-
+
+
+
+
-





-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package tests provides some higher-level tests.
package tests

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/jsonenc"
	_ "zettelstore.de/z/encoder/nativeenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/input"
	_ "zettelstore.de/z/encoder/mdenc"
	_ "zettelstore.de/z/encoder/shtmlenc"
	_ "zettelstore.de/z/encoder/szenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"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"`
}

// exceptions lists all CommonMark tests that should not be tested for identical HTML output
var exceptions = []string{
	" - foo\n   - bar\n\t - baz\n", // 9
	"<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n", // 140
	"<script>\nfoo\n</script>1. *bar*\n",                       // 147
	"- foo\n  - bar\n    - baz\n      - boo\n",                 // 264
	"10) foo\n    - bar\n",                                     // 266
	"- # Foo\n- Bar\n  ---\n  baz\n",                           // 270
	"- foo\n\n- bar\n\n\n- baz\n",                              // 276
	"- foo\n  - bar\n    - baz\n\n\n      bim\n",               // 277
	"1. a\n\n  2. b\n\n   3. c\n",                              // 281
	"1. a\n\n  2. b\n\n    3. c\n",                             // 283
	"- a\n- b\n\n- c\n",                                        // 284
	"* a\n*\n\n* c\n",                                          // 285
	"- a\n- b\n\n  [ref]: /url\n- d\n",                         // 287
	"- a\n  - b\n\n    c\n- d\n",                               // 289
	"* a\n  > b\n  >\n* c\n",                                   // 290
	"- a\n  > b\n  ```\n  c\n  ```\n- d\n",                     // 291
	"- a\n  - b\n",                                             // 293
	"<http://example.com?find=\\*>\n",                          // 306
	"<http://foo.bar.`baz>`\n",                                 // 346
	"[foo<http://example.com/?search=](uri)>\n",                // 522
	"[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 534
	"<http://foo.bar.baz/test?q=hello&id=22&boolean>\n",        // 591
}

var reHeadingID = regexp.MustCompile(` id="[^"]*"`)

func TestEncoderAvailability(t *testing.T) {
	t.Parallel()
	encoderMissing := false
	for _, format := range formats {
		enc := encoder.Create(format, nil)
	for _, enc := range encodings {
		enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		if enc == nil {
			t.Errorf("No encoder for %q found", format)
			t.Errorf("No encoder for %q found", enc)
			encoderMissing = true
		}
	}
	if encoderMissing {
		panic("At least one encoder is missing. See test log")
	}
}

func TestMarkdownSpec(t *testing.T) {
	t.Parallel()
	content, err := os.ReadFile("../testdata/markdown/spec.json")
	if err != nil {
		panic(err)
	}
	var testcases []markdownTestCase
	if err = json.Unmarshal(content, &testcases); err != nil {
		panic(err)
	}
	excMap := make(map[string]bool, len(exceptions))
	for _, exc := range exceptions {
		excMap[exc] = true
	}

	for _, tc := range testcases {
		ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown")
		testAllEncodings(t, tc, ast)
		ast := createMDBlockSlice(tc.Markdown, config.NoHTML)
		testAllEncodings(t, tc, &ast)
		if _, found := excMap[tc.Markdown]; !found {
			testHTMLEncoding(t, tc, ast)
		}
		testZmkEncoding(t, tc, &ast)
	}
		testZmkEncoding(t, tc, ast)
	}
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) {
}

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 sb strings.Builder
	testID := tc.Example*100 + 1
	for _, format := range formats {
		t.Run(fmt.Sprintf("Encode %v %v", format, testID), func(st *testing.T) {
			encoder.Create(format, nil).WriteBlocks(&sb, ast)
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) {
	htmlEncoder := encoder.Create("html", &encoder.Environment{Xhtml: true})
	var sb strings.Builder
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) {
		htmlEncoder.WriteBlocks(&sb, ast)
		gotHTML := sb.String()
		sb.Reset()

		mdHTML := tc.HTML
		mdHTML = strings.ReplaceAll(mdHTML, "\"MAILTO:", "\"mailto:")
		gotHTML = strings.ReplaceAll(gotHTML, " class=\"zs-external\"", "")
		gotHTML = strings.ReplaceAll(gotHTML, "%2A", "*") // url.QueryEscape
		if strings.Count(gotHTML, "<h") > 0 {
			gotHTML = reHeadingID.ReplaceAllString(gotHTML, "")
		}
		if gotHTML != mdHTML {
			mdHTML = strings.ReplaceAll(mdHTML, "<li>\n", "<li>")
			if gotHTML != mdHTML {
				st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML)
			}
		}
	})
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) {
	zmkEncoder := encoder.Create("zmk", nil)
	var sb strings.Builder
func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	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(&sb, ast)
		gotFirst := sb.String()
		zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()
		sb.Reset()

		testID = tc.Example*100 + 2
		secondAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk")
		zmkEncoder.WriteBlocks(&sb, secondAst)
		gotSecond := sb.String()
		secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &secondAst)
		gotSecond := buf.String()
		sb.Reset()

		// if gotFirst != gotSecond {
		// 	st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond)
		// }

		testID = tc.Example*100 + 3
		thirdAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk")
		zmkEncoder.WriteBlocks(&sb, thirdAst)
		gotThird := sb.String()
		thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &thirdAst)
		gotThird := buf.String()
		sb.Reset()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
		}
	})

}
}

func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, `abc@@<br>@@{="html"}def`},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		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)
		}
	}
}

Added 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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) 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	_ "zettelstore.de/z/cmd"
	"zettelstore.de/z/encoder"
	"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")
	file, err := os.Open(fpath)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		if text := scanner.Text(); text != "" && text[0] != '#' {
			result = append(result, text)
		}
	}
	return result, scanner.Err()
}

func getAllParser() (result []*parser.Info) {
	for _, pname := range parser.GetSyntaxes() {
		pinfo := parser.Get(pname)
		if pname == pinfo.Name {
			result = append(result, pinfo)
		}
	}
	return result
}

func getAllEncoder() (result []encoder.Encoder) {
	for _, enc := range encoder.GetEncodings() {
		e := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		result = append(result, e)
	}
	return result
}

func TestNaughtyStringParser(t *testing.T) {
	blns, err := getNaughtyStrings()
	if err != nil {
		t.Fatal(err)
	}
	if len(blns) == 0 {
		t.Fatal("no naughty strings found")
	}
	pinfos := getAllParser()
	if len(pinfos) == 0 {
		t.Fatal("no parser found")
	}
	encs := getAllEncoder()
	if len(encs) == 0 {
		t.Fatal("no encoder found")
	}
	for _, s := range blns {
		for _, pinfo := range pinfos {
			bs := pinfo.ParseBlocks(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name)
			is := pinfo.ParseInlines(input.NewInput([]byte(s)), pinfo.Name)
			for _, enc := range encs {
				_, err = enc.WriteBlocks(io.Discard, &bs)
				if err != nil {
					t.Error(err)
				}
				_, err = enc.WriteInlines(io.Discard, &is)
				if err != nil {
					t.Error(err)
				}
			}
		}
	}
}

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
49
50
51
52
53






54
55
56
57
58



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
1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28



29
30
31
32
33
34


35
36
37






38


39
40



41
42
43
44
45
46
47
48
49
50
51
52
53

54
55
56
57
58
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

-
+

-
+




+
+
+















+

-
-
-
+
+
+



-
-
+
+

-
-
-
-
-
-
+
-
-


-
-
-
+
+
+
+
+
+
+






-
+
+
+
+
+
+


-
-
-
+
+
+

-
-
-
+
+
+

-
+


-
+




-
-
+
+



-
+


















-
+













-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+







+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+



+
-
-
+
+
+







-
-
-
-
-
+
+
+
+
+


-
-
-

-
+
-
-
-
-
+
+
+
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+

+
-
-
+
+
+




+




-
-
-
+
+
+


//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package tests provides some higher-level tests.
package tests

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/jsonenc"
	_ "zettelstore.de/z/encoder/nativeenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	_ "zettelstore.de/z/parser/blob"
	_ "zettelstore.de/z/box/dirbox"
	_ "zettelstore.de/z/parser/zettelmark"
	_ "zettelstore.de/z/place/dirplace"
)

var formats = []string{"html", "djson", "native", "text"}

func getFilePlaces(wd string, kind string) (root string, places []place.ManagedPlace) {
var encodings = []api.EncodingEnum{
	api.EncoderHTML,
	api.EncoderSz,
	api.EncoderText,
}

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)
	}

	cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil}
	cdata := manager.ConnectData{
		Number:   0,
		Config:   testConfig,
		Enricher: &noEnrich{},
		Notify:   nil,
	}
	for _, entry := range entries {
		if entry.IsDir() {
			u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.PlaceDirTypeSimple)
			if err != nil {
				panic(err)
			u, err2 := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple)
			if err2 != nil {
				panic(err2)
			}
			place, err := manager.Connect(u, &noAuth{}, &cdata)
			if err != nil {
				panic(err)
			box, err2 := manager.Connect(u, &noAuth{}, &cdata)
			if err2 != nil {
				panic(err2)
			}
			places = append(places, place)
			boxes = append(boxes, box)
		}
	}
	return root, places
	return root, boxes
}

type noEnrich struct{}

func (nf *noEnrich) Enrich(ctx context.Context, m *meta.Meta) {}
func (nf *noEnrich) Remove(ctx context.Context, m *meta.Meta) {}
func (*noEnrich) Enrich(context.Context, *meta.Meta, int) {}
func (*noEnrich) Remove(context.Context, *meta.Meta)      {}

type noAuth struct{}

func (na *noAuth) IsReadonly() bool { return false }
func (*noAuth) IsReadonly() bool { return false }

func trimLastEOL(s string) string {
	if lastPos := len(s) - 1; lastPos >= 0 && s[lastPos] == '\n' {
		return s[:lastPos]
	}
	return s
}

func resultFile(file string) (data string, err error) {
	f, err := os.Open(file)
	if err != nil {
		return "", err
	}
	defer f.Close()
	src, err := io.ReadAll(f)
	return string(src), err
}

func checkFileContent(t *testing.T, filename string, gotContent string) {
func checkFileContent(t *testing.T, filename, gotContent string) {
	t.Helper()
	wantContent, err := resultFile(filename)
	if err != nil {
		t.Error(err)
		return
	}
	gotContent = trimLastEOL(gotContent)
	wantContent = trimLastEOL(wantContent)
	if gotContent != wantContent {
		t.Errorf("\nWant: %q\nGot:  %q", wantContent, gotContent)
	}
}

func checkBlocksFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) {
	t.Helper()
	var env encoder.Environment
	if enc := encoder.Create(format, &env); enc != nil {
		var sb strings.Builder
		enc.WriteBlocks(&sb, zn.Ast)
		checkFileContent(t, resultName, sb.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer format %q", format))
}

func checkZmkEncoder(t *testing.T, zn *ast.ZettelNode) {
	zmkEncoder := encoder.Create("zmk", nil)
	var sb strings.Builder
	zmkEncoder.WriteBlocks(&sb, zn.Ast)
	gotFirst := sb.String()
	sb.Reset()

	newZettel := parser.ParseZettel(domain.Zettel{
		Meta: zn.Meta, Content: domain.NewContent("\n" + gotFirst)}, "", testConfig)
	zmkEncoder.WriteBlocks(&sb, newZettel.Ast)
	gotSecond := sb.String()
	sb.Reset()

	if gotFirst != gotSecond {
		t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond)
	}
}

func getPlaceName(p place.ManagedPlace, root string) string {
func getBoxName(p box.ManagedBox, root string) string {
	u, err := url.Parse(p.Location())
	if err != nil {
		panic("Unable to parse URL '" + p.Location() + "': " + err.Error())
	}
	return u.Path[len(root):]
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
	t.Helper()
func match(*meta.Meta) bool { return true }

func checkContentPlace(t *testing.T, p place.ManagedPlace, wd, placeName string) {
	ss := p.(place.StartStopper)

	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{}
	metaList, err := p.SelectMeta(context.Background(), match)
	if err != nil {
	if err := p.ApplyMeta(context.Background(),
		func(m *meta.Meta) { metaList = append(metaList, m) },
		query.AlwaysIncluded); err != nil {
		panic(err)
	}
	for _, meta := range metaList {
		zettel, err := p.GetZettel(context.Background(), meta.Zid)
		if err != nil {
			panic(err)
		}
		z := parser.ParseZettel(zettel, "", testConfig)
		for _, format := range formats {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "content", placeName, z.Zid.String()+"."+format)
				checkBlocksFile(st, resultName, z, format)
		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)
			})
		}
		t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) {
			checkZmkEncoder(st, z)
		})
	}
	if err := ss.Stop(context.Background()); err != nil {
	ss.Stop(context.Background())
		panic(err)
	}
}

}

type myConfig struct{}
func TestContentRegression(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}

	root, places := getFilePlaces(wd, "content")
	for _, p := range places {
		checkContentPlace(t, p, wd, getPlaceName(p, root))
	}
}

func (*myConfig) Get(context.Context, *meta.Meta, string) string { return "" }
func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) {
	t.Helper()

func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta {
	if enc := encoder.Create(format, nil); enc != nil {
		var sb strings.Builder
		enc.WriteMeta(&sb, zn.Meta)
		checkFileContent(t, resultName, sb.String())
		return
	return m
	}
	panic(fmt.Sprintf("Unknown writer format %q", format))
}

func checkMetaPlace(t *testing.T, p place.ManagedPlace, wd, placeName string) {
	ss := p.(place.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList, err := p.SelectMeta(context.Background(), match)
	if err != nil {
		panic(err)
	}
}
	for _, meta := range metaList {
		zettel, err := p.GetZettel(context.Background(), meta.Zid)
		if err != nil {
			panic(err)
		}
		z := parser.ParseZettel(zettel, "", testConfig)
		for _, format := range formats {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "meta", placeName, z.Zid.String()+"."+format)
				checkMetaFile(st, resultName, z, format)
			})
		}
	}
	if err := ss.Stop(context.Background()); err != nil {
		panic(err)
	}
}

func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML }
type myConfig struct{}

func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m }
func (cfg *myConfig) GetDefaultTitle() string                  { return "" }
func (cfg *myConfig) GetDefaultRole() string                   { return meta.ValueRoleZettel }
func (cfg *myConfig) GetDefaultSyntax() string                 { return meta.ValueSyntaxZmk }
func (cfg *myConfig) GetDefaultLang() string                   { return "" }
func (cfg *myConfig) GetDefaultVisibility() meta.Visibility    { return meta.VisibilityPublic }
func (cfg *myConfig) GetFooterHTML() string                    { return "" }
func (cfg *myConfig) GetHomeZettel() id.Zid                    { return id.Invalid }
func (cfg *myConfig) GetListPageSize() int                     { return 0 }
func (*myConfig) GetListPageSize() int                     { return 0 }
func (cfg *myConfig) GetMarkerExternal() string                { return "" }
func (cfg *myConfig) GetSiteName() string                      { return "" }
func (cfg *myConfig) GetYAMLHeader() bool                      { return false }
func (cfg *myConfig) GetZettelFileSyntax() []string            { return nil }
func (*myConfig) GetSiteName() string                      { return "" }
func (*myConfig) GetYAMLHeader() bool                      { return false }
func (*myConfig) GetZettelFileSyntax() []string            { return nil }

func (*myConfig) GetSimpleMode() bool                      { return false }
func (cfg *myConfig) GetExpertMode() bool                      { return false }
func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() }
func (*myConfig) GetExpertMode() bool                      { return false }
func (*myConfig) GetVisibility(*meta.Meta) meta.Visibility { return meta.VisibilityPublic }
func (*myConfig) GetMaxTransclusions() int                 { return 1024 }

var testConfig = &myConfig{}

func TestMetaRegression(t *testing.T) {
	t.Parallel()
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, places := getFilePlaces(wd, "meta")
	for _, p := range places {
		checkMetaPlace(t, p, wd, getPlaceName(p, root))
	root, boxes := getFileBoxes(wd, "meta")
	for _, p := range boxes {
		checkMetaBox(t, p, wd, getBoxName(p, root))
	}
}

Deleted tests/result/content/blockcomment/20200215204700.djson.

1

-
[{"t":"CommentBlock","l":["No render"]},{"t":"CommentBlock","a":{"-":""},"l":["Render"]}]

Deleted tests/result/content/blockcomment/20200215204700.html.

1
2
3



-
-
-
<!--
Render
-->

Deleted tests/result/content/blockcomment/20200215204700.native.

1
2


-
-
[CommentBlock "No render"],
[CommentBlock ("",[-]) "Render"]

Deleted tests/result/content/blockcomment/20200215204700.text.

Deleted tests/result/content/cite/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Cite","a":{"-":""},"s":"Stern18"}]}]

Deleted tests/result/content/cite/20200215204700.html.

1

-
<p>Stern18</p>

Deleted tests/result/content/cite/20200215204700.native.

1

-
[Para Cite ("",[-]) "Stern18"]

Deleted tests/result/content/cite/20200215204700.text.

Deleted tests/result/content/comment/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"%"},{"t":"Space"},{"t":"Text","s":"No"},{"t":"Space"},{"t":"Text","s":"comment"},{"t":"Soft"},{"t":"Comment","s":"Comment"}]}]

Deleted tests/result/content/comment/20200215204700.html.

1
2


-
-
<p>% No comment
<!-- Comment --></p>

Deleted tests/result/content/comment/20200215204700.native.

1

-
[Para Text "%",Space,Text "No",Space,Text "comment",Space,Comment "Comment"]

Deleted tests/result/content/comment/20200215204700.text.

1

-
% No comment 

Deleted tests/result/content/descrlist/20200226122100.djson.

1

-
[{"t":"DescriptionList","g":[[[{"t":"Text","s":"Zettel"}],[{"t":"Para","i":[{"t":"Text","s":"Paper"}]}],[{"t":"Para","i":[{"t":"Text","s":"Note"}]}]],[[{"t":"Text","s":"Zettelkasten"}],[{"t":"Para","i":[{"t":"Text","s":"Slip"},{"t":"Space"},{"t":"Text","s":"box"}]}]]]}]

Deleted tests/result/content/descrlist/20200226122100.html.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
<dl>
<dt>Zettel</dt>
<dd>Paper</dd>
<dd>Note</dd>
<dt>Zettelkasten</dt>
<dd>Slip box</dd>
</dl>

Deleted tests/result/content/descrlist/20200226122100.native.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
[DescriptionList
 [Term [Text "Zettel"],
  [Description
   [Para Text "Paper"]],
  [Description
   [Para Text "Note"]]],
 [Term [Text "Zettelkasten"],
  [Description
   [Para Text "Slip",Space,Text "box"]]]]

Deleted tests/result/content/descrlist/20200226122100.text.

1
2
3
4
5





-
-
-
-
-
Zettel
Paper
Note
Zettelkasten
Slip box

Deleted tests/result/content/edit/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Delete","i":[{"t":"Text","s":"delete"}]},{"t":"Soft"},{"t":"Insert","i":[{"t":"Text","s":"insert"}]},{"t":"Soft"},{"t":"Delete","i":[{"t":"Text","s":"kill"}]},{"t":"Insert","i":[{"t":"Text","s":"create"}]}]}]

Deleted tests/result/content/edit/20200215204700.html.

1
2
3



-
-
-
<p><del>delete</del>
<ins>insert</ins>
<del>kill</del><ins>create</ins></p>

Deleted tests/result/content/edit/20200215204700.native.

1

-
[Para Delete [Text "delete"],Space,Insert [Text "insert"],Space,Delete [Text "kill"],Insert [Text "create"]]

Deleted tests/result/content/edit/20200215204700.text.

1

-
delete insert killcreate

Deleted tests/result/content/footnote/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Footnote","a":{"":"sidebar"},"i":[{"t":"Text","s":"foot"}]}]}]

Deleted tests/result/content/footnote/20200215204700.html.

1
2
3
4




-
-
-
-
<p>Text<sup id="fnref:1"><a href="#fn:1" class="zs-footnote-ref" role="doc-noteref">1</a></sup></p>
<ol class="zs-endnotes">
<li id="fn:1" role="doc-endnote">foot <a href="#fnref:1" class="zs-footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></li>
</ol>

Deleted tests/result/content/footnote/20200215204700.native.

1

-
[Para Text "Text",Footnote ("sidebar",[]) [Text "foot"]]

Deleted tests/result/content/footnote/20200215204700.text.

1

-
Text foot

Deleted tests/result/content/format/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Italic","i":[{"t":"Text","s":"italic"}]},{"t":"Soft"},{"t":"Emph","i":[{"t":"Text","s":"emph"}]},{"t":"Soft"},{"t":"Bold","i":[{"t":"Text","s":"bold"}]},{"t":"Soft"},{"t":"Strong","i":[{"t":"Text","s":"strong"}]},{"t":"Soft"},{"t":"Underline","i":[{"t":"Text","s":"unterline"}]},{"t":"Soft"},{"t":"Strikethrough","i":[{"t":"Text","s":"strike"}]},{"t":"Soft"},{"t":"Mono","i":[{"t":"Text","s":"monospace"}]},{"t":"Soft"},{"t":"Super","i":[{"t":"Text","s":"superscript"}]},{"t":"Soft"},{"t":"Sub","i":[{"t":"Text","s":"subscript"}]},{"t":"Soft"},{"t":"Quote","i":[{"t":"Text","s":"Quotes"}]},{"t":"Soft"},{"t":"Quotation","i":[{"t":"Text","s":"Quotation"}]},{"t":"Soft"},{"t":"Small","i":[{"t":"Text","s":"small"}]},{"t":"Soft"},{"t":"Span","i":[{"t":"Text","s":"span"}]},{"t":"Soft"},{"t":"Code","s":"code"},{"t":"Soft"},{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Output","s":"output"}]}]

Deleted tests/result/content/format/20200215204700.html.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<p><i>italic</i>
<em>emph</em>
<b>bold</b>
<strong>strong</strong>
<u>unterline</u>
<s>strike</s>
<span style="font-family:monospace">monospace</span>
<sup>superscript</sup>
<sub>subscript</sub>
"Quotes"
<q>Quotation</q>
<small>small</small>
<span>span</span>
<code>code</code>
<kbd>input</kbd>
<samp>output</samp></p>

Deleted tests/result/content/format/20200215204700.native.

1

-
[Para Italic [Text "italic"],Space,Emph [Text "emph"],Space,Bold [Text "bold"],Space,Strong [Text "strong"],Space,Underline [Text "unterline"],Space,Strikethrough [Text "strike"],Space,Mono [Text "monospace"],Space,Super [Text "superscript"],Space,Sub [Text "subscript"],Space,Quote [Text "Quotes"],Space,Quotation [Text "Quotation"],Space,Small [Text "small"],Space,Span [Text "span"],Space,Code "code",Space,Input "input",Space,Output "output"]

Deleted tests/result/content/format/20200215204700.text.

1

-
italic emph bold strong unterline strike monospace superscript subscript Quotes Quotation small span code input output

Deleted tests/result/content/format/20201107164400.djson.

1

-
[{"t":"Para","i":[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]}]

Deleted tests/result/content/format/20201107164400.html.

1

-
<p><span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span></p>

Deleted tests/result/content/format/20201107164400.native.

1

-
[Para Span ("",[lang="fr"]) [Quote [Text "abc"]]]

Deleted tests/result/content/format/20201107164400.text.

1

-
abc

Deleted tests/result/content/heading/20200215204700.djson.

1

-
[{"t":"Heading","n":2,"s":"first","i":[{"t":"Text","s":"First"}]}]

Deleted tests/result/content/heading/20200215204700.html.

1

-
<h2 id="first">First</h2>

Deleted tests/result/content/heading/20200215204700.native.

1

-
[Heading 2 "first" Text "First"]

Deleted tests/result/content/heading/20200215204700.text.

1

-
First

Deleted tests/result/content/hrule/20200215204700.djson.

1

-
[{"t":"Hrule"}]

Deleted tests/result/content/hrule/20200215204700.html.

1

-
<hr>

Deleted tests/result/content/hrule/20200215204700.native.

1

-
[Hrule]

Deleted tests/result/content/hrule/20200215204700.text.

Deleted tests/result/content/image/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Image","s":"abc"}]}]

Deleted tests/result/content/image/20200215204700.html.

1

-
<p><img src="abc" alt=""></p>

Deleted tests/result/content/image/20200215204700.native.

1

-
[Para Image "abc"]

Deleted tests/result/content/image/20200215204700.text.

Deleted tests/result/content/link/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Link","q":"external","s":"https://zettelstore.de/z","i":[{"t":"Text","s":"Home"}]},{"t":"Soft"},{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"Frag"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]},{"t":"Soft"},{"t":"Link","q":"based","s":"/based","i":[{"t":"Text","s":"B"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"../rel","i":[{"t":"Text","s":"R"}]}]}]

Deleted tests/result/content/link/20200215204700.html.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
<p><a href="https://zettelstore.de/z" class="zs-external">Home</a>
<a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a>
<a href="00000000000100">Config</a>
<a href="00000000000100">00000000000100</a>
<a href="#frag">Frag</a>
<a href="#frag">#frag</a>
<a href="/hosted">H</a>
<a href="/based">B</a>
<a href="../rel">R</a></p>

Deleted tests/result/content/link/20200215204700.native.

1

-
[Para Link EXTERNAL "https://zettelstore.de/z" [Text "Home"],Space,Link EXTERNAL "https://zettelstore.de" [],Space,Link ZETTEL "00000000000100" [Text "Config"],Space,Link ZETTEL "00000000000100" [],Space,Link SELF "#frag" [Text "Frag"],Space,Link SELF "#frag" [],Space,Link LOCAL "/hosted" [Text "H"],Space,Link BASED "/based" [Text "B"],Space,Link LOCAL "../rel" [Text "R"]]

Deleted tests/result/content/link/20200215204700.text.

1

-
Home  Config  Frag  H B R

Deleted tests/result/content/list/20200215204700.djson.

1

-
[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"3"}]}]]}]

Deleted tests/result/content/list/20200215204700.html.

1
2
3
4
5





-
-
-
-
-
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>

Deleted tests/result/content/list/20200215204700.native.

1
2
3
4




-
-
-
-
[BulletList
 [[Para Text "Item",Space,Text "1"]],
 [[Para Text "Item",Space,Text "2"]],
 [[Para Text "Item",Space,Text "3"]]]

Deleted tests/result/content/list/20200215204700.text.

1
2
3



-
-
-
Item 1
Item 2
Item 3

Deleted tests/result/content/list/20200217194800.djson.

1

-
[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item1.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.3"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.2"}]}]]}]

Deleted tests/result/content/list/20200217194800.html.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
<ul>
<li>Item1.1</li>
<li>Item1.2</li>
<li>Item1.3</li>
<li>Item2.1</li>
<li>Item2.2</li>
</ul>

Deleted tests/result/content/list/20200217194800.native.

1
2
3
4
5
6






-
-
-
-
-
-
[BulletList
 [[Para Text "Item1.1"]],
 [[Para Text "Item1.2"]],
 [[Para Text "Item1.3"]],
 [[Para Text "Item2.1"]],
 [[Para Text "Item2.2"]]]

Deleted tests/result/content/list/20200217194800.text.

1
2
3
4
5





-
-
-
-
-
Item1.1
Item1.2
Item1.3
Item2.1
Item2.2

Deleted tests/result/content/list/20200516105700.djson.

1

-
[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T1"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T2"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T3"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T4"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T5"}]}]]}]

Deleted tests/result/content/list/20200516105700.html.

1
2
3
4
5
6
7
8
9
10
11
12
13
14














-
-
-
-
-
-
-
-
-
-
-
-
-
-
<ul>
<li><p>T1</p>
<ul>
<li>T2</li>
</ul>
</li>
<li><p>T3</p>
<ul>
<li>T4</li>
</ul>
</li>
<li><p>T5</p>
</li>
</ul>

Deleted tests/result/content/list/20200516105700.native.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
[BulletList
 [[Para Text "T1"],
  [BulletList
   [[Para Text "T2"]]]],
 [[Para Text "T3"],
  [BulletList
   [[Para Text "T4"]]]],
 [[Para Text "T5"]]]

Deleted tests/result/content/list/20200516105700.text.

1
2
3
4
5





-
-
-
-
-
T1
T2
T3
T4
T5

Deleted tests/result/content/literal/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Code","s":"program"},{"t":"Soft"},{"t":"Output","s":"output"}]}]

Deleted tests/result/content/literal/20200215204700.html.

1
2
3



-
-
-
<p><kbd>input</kbd>
<code>program</code>
<samp>output</samp></p>

Deleted tests/result/content/literal/20200215204700.native.

1

-
[Para Input "input",Space,Code "program",Space,Output "output"]

Deleted tests/result/content/literal/20200215204700.text.

1

-
input program output

Deleted tests/result/content/mark/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Mark","s":"mark"}]}]

Deleted tests/result/content/mark/20200215204700.html.

1

-
<p><a id="mark"></a></p>

Deleted tests/result/content/mark/20200215204700.native.

1

-
[Para Mark "mark"]

Deleted tests/result/content/mark/20200215204700.text.

Deleted tests/result/content/paragraph/20200215185900.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"This"},{"t":"Space"},{"t":"Text","s":"is"},{"t":"Space"},{"t":"Text","s":"a"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"for"},{"t":"Space"},{"t":"Text","s":"testing."}]}]

Deleted tests/result/content/paragraph/20200215185900.html.

1

-
<p>This is a zettel for testing.</p>

Deleted tests/result/content/paragraph/20200215185900.native.

1

-
[Para Text "This",Space,Text "is",Space,Text "a",Space,Text "zettel",Space,Text "for",Space,Text "testing."]

Deleted tests/result/content/paragraph/20200215185900.text.

1

-
This is a zettel for testing.

Deleted tests/result/content/paragraph/20200217151800.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"},{"t":"Soft"},{"t":"Text","s":"*abc"}]},{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"abc"}]}]]}]

Deleted tests/result/content/paragraph/20200217151800.html.

1
2
3
4
5
6






-
-
-
-
-
-
<p>Text Text
*abc</p>
<p>Text Text</p>
<ul>
<li>abc</li>
</ul>

Deleted tests/result/content/paragraph/20200217151800.native.

1
2
3
4




-
-
-
-
[Para Text "Text",Space,Text "Text",Space,Text "*abc"],
[Para Text "Text",Space,Text "Text"],
[BulletList
 [[Para Text "abc"]]]

Deleted tests/result/content/paragraph/20200217151800.text.

1
2
3



-
-
-
Text Text *abc
Text Text
abc

Deleted tests/result/content/png/20200512180900.djson.

1

-
[{"t":"Blob","q":"20200512180900","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]

Deleted tests/result/content/png/20200512180900.html.

1

-
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="20200512180900">

Deleted tests/result/content/png/20200512180900.native.

1

-
[BLOB "20200512180900" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="]

Deleted tests/result/content/png/20200512180900.text.

Deleted tests/result/content/quoteblock/20200215204700.djson.

1

-
[{"t":"QuoteBlock","b":[{"t":"Para","i":[{"t":"Text","s":"To"},{"t":"Space"},{"t":"Text","s":"be"},{"t":"Space"},{"t":"Text","s":"or"},{"t":"Space"},{"t":"Text","s":"not"},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"be."}]}],"i":[{"t":"Text","s":"Romeo"}]}]

Deleted tests/result/content/quoteblock/20200215204700.html.

1
2
3
4




-
-
-
-
<blockquote>
<p>To be or not to be.</p>
<cite>Romeo</cite>
</blockquote>

Deleted tests/result/content/quoteblock/20200215204700.native.

1
2
3



-
-
-
[QuoteBlock
 [[Para Text "To",Space,Text "be",Space,Text "or",Space,Text "not",Space,Text "to",Space,Text "be."]],
 [Cite Text "Romeo"]]

Deleted tests/result/content/quoteblock/20200215204700.text.

1
2


-
-
To be or not to be.
Romeo

Deleted tests/result/content/spanblock/20200215204700.djson.

1

-
[{"t":"SpanBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Text","s":"simple"},{"t":"Soft"},{"t":"Space","n":3},{"t":"Text","s":"span"},{"t":"Soft"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"much"},{"t":"Space"},{"t":"Text","s":"more"}]}]}]

Deleted tests/result/content/spanblock/20200215204700.html.

1
2
3
4
5





-
-
-
-
-
<div>
<p>A simple
 span
and much more</p>
</div>

Deleted tests/result/content/spanblock/20200215204700.native.

1
2


-
-
[SpanBlock
 [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]]

Deleted tests/result/content/spanblock/20200215204700.text.

1

-
A simple  span and much more

Deleted tests/result/content/table/20200215204700.djson.

1

-
[{"t":"Table","p":[[],[[["",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],["",[{"t":"Text","s":"c3"}]]]]]}]

Deleted tests/result/content/table/20200215204700.html.

1
2
3
4
5





-
-
-
-
-
<table>
<tbody>
<tr><td>c1</td><td>c2</td><td>c3</td></tr>
</tbody>
</table>

Deleted tests/result/content/table/20200215204700.native.

1
2


-
-
[Table
 [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]]]

Deleted tests/result/content/table/20200215204700.text.

1

-
c1 c2 c3

Deleted tests/result/content/table/20200618140700.djson.

1

-
[{"t":"Table","p":[[[">",[{"t":"Text","s":"h1"}]],["",[{"t":"Text","s":"h2"}]],[":",[{"t":"Text","s":"h3"}]]],[[["<",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],[":",[{"t":"Text","s":"c3"}]]],[[">",[{"t":"Text","s":"f1"}]],["",[{"t":"Text","s":"f2"}]],[":",[{"t":"Text","s":"=f3"}]]]]]}]

Deleted tests/result/content/table/20200618140700.html.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
<table>
<thead>
<tr><th style="text-align:right">h1</th><th>h2</th><th style="text-align:center">h3</th></tr>
</thead>
<tbody>
<tr><td style="text-align:left">c1</td><td>c2</td><td style="text-align:center">c3</td></tr>
<tr><td style="text-align:right">f1</td><td>f2</td><td style="text-align:center">=f3</td></tr>
</tbody>
</table>

Deleted tests/result/content/table/20200618140700.native.

1
2
3
4




-
-
-
-
[Table
 [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]],
 [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]],
 [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]]

Deleted tests/result/content/table/20200618140700.text.

1
2
3



-
-
-
h1 h2 h3
c1 c2 c3
f1 f2 =f3

Deleted tests/result/content/verbatim/20200215204700.djson.

1

-
[{"t":"CodeBlock","l":["if __name__ == \"main\":","  print(\"Hello, World\")","exit(0)"]}]

Deleted tests/result/content/verbatim/20200215204700.html.

1
2
3
4




-
-
-
-
<pre><code>if __name__ == &quot;main&quot;:
  print(&quot;Hello, World&quot;)
exit(0)
</code></pre>

Deleted tests/result/content/verbatim/20200215204700.native.

1

-
[CodeBlock "if __name__ == \"main\":\n  print(\"Hello, World\")\nexit(0)"]

Deleted tests/result/content/verbatim/20200215204700.text.

1
2
3



-
-
-
if __name__ == "main":
  print("Hello, World")
exit(0)

Deleted tests/result/content/verseblock/20200215204700.djson.

1

-
[{"t":"VerseBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A line"},{"t":"Hard"},{"t":"Text","s":"  another line"},{"t":"Hard"},{"t":"Text","s":"Back"}]},{"t":"Para","i":[{"t":"Text","s":"Paragraph"}]},{"t":"Para","i":[{"t":"Text","s":"    Spacy  Para"}]}],"i":[{"t":"Text","s":"Author"}]}]

Deleted tests/result/content/verseblock/20200215204700.html.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
<div>
<p>A line<br>
  another line<br>
Back</p>
<p>Paragraph</p>
<p>    Spacy  Para</p>
<cite>Author</cite>
</div>

Deleted tests/result/content/verseblock/20200215204700.native.

1
2
3
4
5





-
-
-
-
-
[VerseBlock
 [[Para Text "A line",Break,Text "  another line",Break,Text "Back"],
  [Para Text "Paragraph"],
  [Para Text "    Spacy  Para"]],
 [Cite Text "Author"]]

Deleted tests/result/content/verseblock/20200215204700.text.

1
2
3
4
5
6






-
-
-
-
-
-
A line
  another line
Back
Paragraph
    Spacy  Para
Author

Deleted tests/result/meta/copyright/20200310125800.djson.

1

-
{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","copyright":"(c) 2020 Detlef Stern","license":"CC BY-SA 4.0"}

Added tests/result/meta/copyright/20200310125800.zjson.


1
+
{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","copyright":"(c) 2020 Detlef Stern","license":"CC BY-SA 4.0"}

Deleted tests/result/meta/header/20200310125800.djson.

1

-
{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","x-no":"00000000000000"}

Added tests/result/meta/header/20200310125800.zjson.


1
+
{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","x-no":"00000000000000"}

Deleted tests/result/meta/title/20200310110300.djson.

1

-
{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Italic","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"}

Changes to tests/result/meta/title/20200310110300.native.

1

2
3

1
2
3
-
+


[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Italic [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"]
[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Emph [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"]
[Role "zettel"]
[Syntax "zmk"]

Added tests/result/meta/title/20200310110300.zjson.


1
+
{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Emph","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"}

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






































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package main provides a command to build and run the software.
package main

import (
	"archive/zip"
	"bytes"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"zettelstore.de/z/strfun"
)

func executeCommand(env []string, name string, arg ...string) (string, error) {
	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)
	}
	if len(env) > 0 {
		env = append(env, os.Environ()...)
	}
	var out bytes.Buffer
	cmd := exec.Command(name, arg...)
	cmd.Env = env
	cmd.Stdin = nil
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	return out.String(), err
}

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
}

var fossilCheckout = regexp.MustCompile(`^checkout:\s+([0-9a-f]+)\s`)
var dirtyPrefixes = []string{
	"DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "}

const dirtySuffix = "-dirty"

func readFossilVersion() (string, error) {
	s, err := executeCommand(nil, "fossil", "status", "--differ")
	if err != nil {
		return "", err
	}
	var hash, suffix string
	for _, line := range strfun.SplitLines(s) {
		if hash == "" {
			if m := fossilCheckout.FindStringSubmatch(line); len(m) > 0 {
				hash = m[1][:10]
				if suffix != "" {
					return hash + suffix, nil
				}
				continue
			}
		}
		if suffix == "" {
			for _, prefix := range dirtyPrefixes {
				if strings.HasPrefix(line, prefix) {
					suffix = dirtySuffix
					if hash != "" {
						return hash + suffix, nil
					}
					break
				}
			}
		}
	}
	return hash, nil
}

func getVersionData() (string, string) {
	base, err := readVersionFile()
	if err != nil {
		base = "dev"
	}
	fossil, err := readFossilVersion()
	if err != nil {
		return base, ""
	}
	return base, fossil
}

func calcVersion(base, vcs string) string { return base + "+" + vcs }

func getVersion() string {
	base, vcs := getVersionData()
	return calcVersion(base, vcs)
}

func findExec(cmd string) string {
	if path, err := executeCommand(nil, "which", "shadow"); err == nil && path != "" {
		return path
	}
	return ""
}

func cmdCheck() error {
	if err := checkGoTest(); err != nil {
		return err
	}
	if err := checkGoVet(); err != nil {
		return err
	}
	if err := checkGoLint(); err != nil {
		return err
	}
	if err := checkGoVetShadow(); err != nil {
		return err
	}
	if err := checkStaticcheck(); err != nil {
		return err
	}
	return checkFossilExtra()
}

func checkGoTest() error {
	out, err := executeCommand(nil, "go", "test", "./...")
	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 checkGoLint() error {
	out, err := executeCommand(nil, "golint", "./...")
	if out != "" {
		fmt.Fprintln(os.Stderr, "Some lints failed")
		fmt.Fprint(os.Stderr, out)
	}
	return err
}

func checkGoVetShadow() error {
	path := findExec("shadow")
	if path == "" {
		return nil
	}
	out, err := executeCommand(nil, "go", "vet", "-vettool", strings.TrimSpace(path), "./...")
	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 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
}

func cmdBuild() error {
	return doBuild(nil, getVersion(), "bin/zettelstore")
}

func doBuild(env []string, version, target string) error {
	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, string) {
	base, fossil := getVersionData()
	if strings.HasSuffix(base, "dev") {
		base = base[:len(base)-3] + "preview-" + time.Now().Format("20060102")
	}
	if strings.HasSuffix(fossil, dirtySuffix) {
		fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil)
		base = base + dirtySuffix
	}
	return base, fossil
}

func cmdRelease() error {
	if err := cmdCheck(); err != nil {
		return err
	}
	base, fossil := getReleaseVersionData()
	releases := []struct {
		arch string
		os   string
		env  []string
		name string
	}{
		{"amd64", "linux", nil, "zettelstore"},
		{"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
		{"amd64", "darwin", nil, "iZettelstore"},
		{"arm64", "darwin", nil, "iZettelstore"},
		{"amd64", "windows", nil, "zettelstore.exe"},
	}
	for _, rel := range releases {
		env := append(rel.env, "GOARCH="+rel.arch, "GOOS="+rel.os)
		zsName := filepath.Join("releases", rel.name)
		if err := doBuild(env, calcVersion(base, fossil), 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
		}
	}
	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     Outputs 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.`)
}

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()
		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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/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"},
		{"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, 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"},
	}
	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

import (
	"context"
	"flag"
	"fmt"
	"log"
	"math/rand/v2"
	"net/url"
	"os"
	"regexp"
	"sort"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/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 += 1
			} 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)
	}
	sort.Strings(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]]
	}
	sort.Strings(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('b'), "zettel rename form", 100},
	{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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
)

var EnvDirectProxy = []string{"GOPROXY=direct"}
var EnvGoVCS = []string{"GOVCS=zettelstore.de:fossil"}
var Verbose bool

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
}

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
}

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
}
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 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 forRelease {
		if err := checkGoVulncheck(); err != nil {
			return err
		}
	}
	return checkFossilExtra()
}

func CheckGoTest(pkg string, testParams ...string) error {
	var env []string
	env = append(env, EnvDirectProxy...)
	env = append(env, EnvGoVCS...)
	args := []string{"test", pkg}
	args = append(args, testParams...)
	out, err := ExecuteCommand(env, "go", args...)
	if err != nil {
		for _, line := range strfun.SplitLines(out) {
			if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") {
				continue
			}
			fmt.Fprintln(os.Stderr, line)
		}
	}
	return err
}
func checkGoVet() error {
	out, err := ExecuteCommand(EnvGoVCS, "go", "vet", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some checks failed")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkShadow(forRelease bool) error {
	path, err := findExecStrict("shadow", forRelease)
	if path == "" {
		return err
	}
	out, err := ExecuteCommand(EnvGoVCS, path, "-strict", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some shadowed variables found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkStaticcheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "staticcheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {
		return err
	}
	out, err := ExecuteCommand(EnvGoVCS, path, "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some unparam problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	if forRelease {
		if out2, err2 := ExecuteCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
			fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
			if len(out2) > 0 {
				fmt.Fprintln(os.Stderr, out2)
			}
		}
	}
	return err
}

func checkGoVulncheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "govulncheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some checks failed")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}
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
56
57
58
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














































1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17

18
19
20
21
22
23
24



25
26
27
28






29
30
31
32
33


34
35
36
37

38
39
40
41


42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58
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

-
+

-
+




+
+
+


-




-
+
+


+


-
-
-
+
+
+

-
-
-
-
-
-



+

-
-
+



-
+

+

-
-
+




+
+
+
-
+




+




-
-
+
+

+



-
-
-
+
+
+
+

+


+


+














-
+






+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"
	"math/rand"
	"math/rand/v2"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// AuthenticatePort is the interface used by this use case.
type AuthenticatePort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, s *search.Search) ([]*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(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.
func (uc Authenticate) Run(ctx context.Context, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) {
func (uc *Authenticate) Run(ctx context.Context, r *http.Request, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) {
	identMeta, err := uc.ucGetUser.Run(ctx, ident)
	defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond)

	if identMeta == nil || err != nil {
		uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found")
		compensateCompare()
		return nil, err
	}

	if hashCred, ok := identMeta.Get(meta.KeyCredential); ok {
		ok, err := cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential)
	if hashCred, ok := identMeta.Get(api.KeyCredential); ok {
		ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential)
		if err != nil {
			uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials")
			return nil, err
		}
		if ok {
			token, err := uc.token.GetToken(identMeta, d, k)
			if err != nil {
				return nil, err
			token, err2 := uc.token.GetToken(identMeta, d, k)
			if err2 != nil {
				uc.log.Info().Str("ident", ident).Err(err).Msg("Unable to produce authentication token")
				return nil, err2
			}
			uc.log.Info().Str("user", ident).Msg("Successful")
			return token, nil
		}
		uc.log.Info().Str("ident", ident).HTTPIP(r).Msg("Credentials don't match")
		return nil, nil
	}
	uc.log.Info().Str("ident", ident).Msg("No credential stored")
	compensateCompare()
	return nil, nil
}

// compensateCompare if normal comapare is not possible, to avoid timing hints.
func compensateCompare() {
	cred.CompareHashAndCredential(
		"$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)
	}
}

// IsAuthenticatedPort contains method for this usecase.
type IsAuthenticatedPort interface {
	GetUser(context.Context) *meta.Meta
}

// IsAuthenticated cheks if the caller is alrwady authenticated.
type IsAuthenticated struct {
	log   *logger.Logger
	port  IsAuthenticatedPort
	authz auth.AuthzManager
}

// NewIsAuthenticated creates a new use case object.
func NewIsAuthenticated(log *logger.Logger, port IsAuthenticatedPort, authz auth.AuthzManager) IsAuthenticated {
	return IsAuthenticated{
		log:   log,
		port:  port,
		authz: authz,
	}
}

// IsAuthenticatedResult is an enumeration.
type IsAuthenticatedResult uint8

// Values for IsAuthenticatedResult.
const (
	_ IsAuthenticatedResult = iota
	IsAuthenticatedDisabled
	IsAuthenticatedAndValid
	IsAuthenticatedAndInvalid
)

// Run executes the use case.
func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult {
	if !uc.authz.WithAuth() {
		uc.log.Info().Str("auth", "disabled").Msg("IsAuthenticated")
		return IsAuthenticatedDisabled
	}
	if uc.port.GetUser(ctx) == nil {
		uc.log.Info().Msg("IsAuthenticated is false")
		return IsAuthenticatedAndInvalid
	}
	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








































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the Zettelstore.
package usecase

import (
	"context"

	"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)
}

// ZettelContext is the data for this use case.
type ZettelContext struct {
	port ZettelContextPort
}

// NewZettelContext creates a new use case.
func NewZettelContext(port ZettelContextPort) ZettelContext {
	return ZettelContext{port: port}
}

// 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
)

// ParseZCDirection returns a direction value for a given string.
func ParseZCDirection(s string) ZettelContextDirection {
	switch s {
	case "backward":
		return ZettelContextBackward
	case "forward":
		return ZettelContextForward
	}
	return ZettelContextBoth
}

// Run executes the use case.
func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) {
	start, err := uc.port.GetMeta(ctx, zid)
	if err != nil {
		return nil, err
	}
	tasks := ztlCtx{depth: depth}
	uc.addInitialTasks(ctx, &tasks, start)
	visited := id.NewSet()
	isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward
	isForward := dir == ZettelContextBoth || dir == ZettelContextForward
	for !tasks.empty() {
		m, curDepth := tasks.pop()
		if _, ok := visited[m.Zid]; ok {
			continue
		}
		visited[m.Zid] = true
		result = append(result, m)
		if limit > 0 && len(result) > limit { // start is the first element of result
			break
		}
		curDepth++
		for _, p := range m.PairsRest(true) {
			if p.Key == meta.KeyBackward {
				if isBackward {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
				continue
			}
			if p.Key == meta.KeyForward {
				if isForward {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
				continue
			}
			if p.Key != meta.KeyBack {
				hasInverse := meta.Inverse(p.Key) != ""
				if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
					continue
				}
				if t := meta.Type(p.Key); t == meta.TypeID {
					uc.addID(ctx, &tasks, curDepth, p.Value)
				} else if t == meta.TypeIDSet {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
			}
		}
	}
	return result, nil
}

func (uc ZettelContext) addInitialTasks(ctx context.Context, tasks *ztlCtx, start *meta.Meta) {
	tasks.add(start, 0)
}

func (uc ZettelContext) addID(ctx context.Context, tasks *ztlCtx, depth int, value string) {
	if zid, err := id.Parse(value); err == nil {
		if m, err := uc.port.GetMeta(ctx, zid); err == nil {
			tasks.add(m, depth)
		}
	}
}

func (uc ZettelContext) addIDSet(ctx context.Context, tasks *ztlCtx, depth int, value string) {
	for _, val := range meta.ListFromValue(value) {
		uc.addID(ctx, tasks, depth, val)
	}
}

type ztlCtxTask struct {
	next  *ztlCtxTask
	meta  *meta.Meta
	depth int
}

type ztlCtx struct {
	first *ztlCtxTask
	last  *ztlCtxTask
	depth int
}

func (zc *ztlCtx) add(m *meta.Meta, depth int) {
	if zc.depth > 0 && depth > zc.depth {
		return
	}
	task := &ztlCtxTask{next: nil, meta: m, depth: depth}
	if zc.first == nil {
		zc.first = task
		zc.last = task
	} else {
		zc.last.next = task
		zc.last = task
	}
}

func (zc *ztlCtx) empty() bool {
	return zc.first == nil
}

func (zc *ztlCtx) pop() (*meta.Meta, int) {
	task := zc.first
	if task == nil {
		return nil, -1
	}
	zc.first = task.next
	if zc.first == nil {
		zc.last = nil
	}
	return task.meta, task.depth
}

Deleted usecase/copy_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) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/strfun"
)

// CopyZettel is the data for this use case.
type CopyZettel struct{}

// NewCopyZettel creates a new use case.
func NewCopyZettel() CopyZettel {
	return CopyZettel{}
}

// Run executes the use case.
func (uc CopyZettel) Run(origZettel domain.Zettel) domain.Zettel {
	m := origZettel.Meta.Clone()
	if title, ok := m.Get(meta.KeyTitle); ok {
		if len(title) > 0 {
			title = "Copy of " + title
		} else {
			title = "Copy"
		}
		m.Set(meta.KeyTitle, title)
	}
	content := strfun.TrimSpaceRight(origZettel.Content.AsString())
	return domain.Zettel{Meta: m, Content: domain.Content(content)}
}

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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21




22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/strfun"
	"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(rtConfig config.Config, port CreateZettelPort) CreateZettel {
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 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 zettel.Zettel{Meta: m, Content: content}
}

// PrepareVersion the zettel for further modification.
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 zettel.Zettel{Meta: m, Content: content}
}

// PrepareFolge the zettel for further modification.
func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareChild the zettel for further modification.
func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeySuperior, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta
	m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	updateMetaRoleTagsSyntax(m, om)

	const prefixLen = len(meta.NewPrefix)
	for _, pair := range om.PairsRest() {
		if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
			m.Set(key[prefixLen:], pair.Value)
		}
	}
	if newTitle != "" {
		m.Set(api.KeyTitle, newTitle)
	}
	content := origZettel.Content
	content.TrimSpace()
	return zettel.Zettel{Meta: m, Content: content}
}

func updateMetaRoleTagsSyntax(m, orig *meta.Meta) {
	m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, ""))
	m.SetNonEmpty(api.KeyTags, orig.GetDefault(api.KeyTags, ""))
	m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, meta.DefaultSyntax))
}

func prependTitle(title, s0, s1 string) string {
	if len(title) > 0 {
		return s1 + title
	}
	return s0
}

func setReadonly(m *meta.Meta) {
	if _, found := m.Get(api.KeyReadOnly); found {
		// Currently, "false" is a safe value.
		//
		// If the current user and its role is known, a more elaborative calculation
		// 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
	}

	if title, ok := m.Get(meta.KeyTitle); !ok || title == "" {
		m.Set(meta.KeyTitle, uc.rtConfig.GetDefaultTitle())
	m.Set(api.KeyCreated, time.Now().Local().Format(id.TimestampLayout))
	m.Delete(api.KeyModified)
	}
	if role, ok := m.Get(meta.KeyRole); !ok || role == "" {
		m.Set(meta.KeyRole, uc.rtConfig.GetDefaultRole())
	}
	if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax())
	}
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content = domain.Content(strfun.TrimSpaceRight(zettel.Content.AsString()))
	return uc.port.CreateZettel(ctx, zettel)
	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
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
43
44
45

-
+

-
+




+
+
+


-





+
-
+




-
+





+




-
-
+
+



-
-
+
+
+
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/logger"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/zettel/id"
)

// DeleteZettelPort is the interface used by this use case.
type DeleteZettelPort interface {
	// DeleteZettel removes the zettel from the place.
	// DeleteZettel removes the zettel from the box.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

// DeleteZettel is the data for this use case.
type DeleteZettel struct {
	log  *logger.Logger
	port DeleteZettelPort
}

// NewDeleteZettel creates a new use case.
func NewDeleteZettel(port DeleteZettelPort) DeleteZettel {
	return DeleteZettel{port: port}
func NewDeleteZettel(log *logger.Logger, port DeleteZettelPort) DeleteZettel {
	return DeleteZettel{log: log, port: port}
}

// Run executes the use case.
func (uc DeleteZettel) Run(ctx context.Context, zid id.Zid) error {
	return uc.port.DeleteZettel(ctx, zid)
func (uc *DeleteZettel) Run(ctx context.Context, zid id.Zid) error {
	err := uc.port.DeleteZettel(ctx, zid)
	uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Delete zettel")
	return err
}

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/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
	ucGetZettel *GetZettel
	ucQuery     *Query
}

// NewEvaluate creates a new use case.
func NewEvaluate(rtConfig config.Config, ucGetZettel *GetZettel, ucQuery *Query) Evaluate {
	return Evaluate{
		rtConfig:    rtConfig,
		ucGetZettel: ucGetZettel,
		ucQuery:     ucQuery,
	}
}

// Run executes the use case.
func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
	zettel, err := uc.ucGetZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}
	return uc.RunZettel(ctx, zettel, syntax), nil
}

// RunZettel executes the use case for a given zettel.
func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.ZettelNode {
	zn := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig)
	evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn)
	return zn
}

// RunBlockNode executes the use case for a metadata list.
func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice {
	if bn == nil {
		return nil
	}
	bns := ast.BlockSlice{bn}
	evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns)
	return bns
}

// RunMetadata executes the use case for a metadata value.
func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice {
	is := parser.ParseMetadata(value)
	evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
	return is
}

// GetZettel retrieves the full zettel of a given zettel identifier.
func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	return uc.ucGetZettel.Run(ctx, zid)
}

// QueryMeta returns a list of metadata that comply to the given selection criteria.
func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	return uc.ucQuery.Run(ctx, q)
}

Deleted usecase/folge_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
















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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 provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// FolgeZettel is the data for this use case.
type FolgeZettel struct {
	rtConfig config.Config
}

// NewFolgeZettel creates a new use case.
func NewFolgeZettel(rtConfig config.Config) FolgeZettel {
	return FolgeZettel{rtConfig}
}

// Run executes the use case.
func (uc FolgeZettel) Run(origZettel domain.Zettel) domain.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, ok := origMeta.Get(meta.KeyTitle); ok {
		if len(title) > 0 {
			title = "Folge of " + title
		} else {
			title = "Folge"
		}
		m.Set(meta.KeyTitle, title)
	}
	m.Set(meta.KeyRole, config.GetRole(origMeta, uc.rtConfig))
	m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, ""))
	m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax())
	m.Set(meta.KeyPrecursor, origMeta.Zid.String())
	return domain.Zettel{Meta: m, Content: ""}
}

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
40








































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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 provides (business) use cases for the zettelstore.
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"

	"zettelstore.de/client.fossil/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
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
93
94
95
96

97
98
99
100
101

-
+

-
+




+
+
+


-





+

-
-
-
-
+
+
+
+
+







-
-
+
+















-
+




-
-
+
+
-
-
-
-
-
+


-
-
-
-
+
+














-
+














-
+




+
-
+




//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
	"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, s *search.Search) ([]*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 = place.NoEnrichContext(ctx)
	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(meta.KeyUserID, "") == ident {
	identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner())
	if err == nil && identZettel.Meta.GetDefault(api.KeyUserID, "") == ident {
		if role, ok := identMeta.Get(meta.KeyRole); !ok ||
			role != meta.ValueRoleUser {
			return nil, nil
		}
		return identMeta, nil
		return identZettel.Meta, nil
	}
	// Owner was not found or has another ident. Try via list search.
	var s *search.Search
	s = s.AddExpr(meta.KeyRole, meta.ValueRoleUser)
	s = s.AddExpr(meta.KeyUserID, ident)
	metaList, err := uc.port.SelectMeta(ctx, s)
	q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident)
	metaList, err := uc.port.SelectMeta(ctx, nil, q)
	if err != nil {
		return nil, err
	}
	if len(metaList) < 1 {
		return nil, nil
	}
	return metaList[len(metaList)-1], nil
}

// Use case: return an user identified by zettel id and assert given ident value.
// ------------------------------------------------------------------------------

// GetUserByZidPort is the interface used by this use case.
type GetUserByZidPort interface {
	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(place.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(meta.KeyUserID); !ok || val != ident {
	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
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

-
+

-
+




+
+
+


-





-
-
+
+





-
+













-
+


//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
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)
}

Deleted usecase/list_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








































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
)

// ListMetaPort is the interface used by this use case.
type ListMetaPort interface {
	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// 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, s *search.Search) ([]*meta.Meta, error) {
	return uc.port.SelectMeta(ctx, s)
}

Deleted usecase/list_role.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"
	"sort"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
)

// ListRolePort is the interface used by this use case.
type ListRolePort interface {
	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListRole is the data for this use case.
type ListRole struct {
	port ListRolePort
}

// NewListRole creates a new use case.
func NewListRole(port ListRolePort) ListRole {
	return ListRole{port: port}
}

// Run executes the use case.
func (uc ListRole) Run(ctx context.Context) ([]string, error) {
	metas, err := uc.port.SelectMeta(place.NoEnrichContext(ctx), nil)
	if err != nil {
		return nil, err
	}
	roles := make(map[string]bool, 8)
	for _, m := range metas {
		if role, ok := m.Get(meta.KeyRole); ok && role != "" {
			roles[role] = true
		}
	}
	result := make([]string, 0, len(roles))
	for role := range roles {
		result = append(result, role)
	}
	sort.Strings(result)
	return result, nil
}

Deleted usecase/list_tags.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
)

// ListTagsPort is the interface used by this use case.
type ListTagsPort interface {
	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListTags is the data for this use case.
type ListTags struct {
	port ListTagsPort
}

// NewListTags creates a new use case.
func NewListTags(port ListTagsPort) ListTags {
	return ListTags{port: port}
}

// TagData associates tags with a list of all zettel meta that use this tag
type TagData map[string][]*meta.Meta

// Run executes the use case.
func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) {
	metas, err := uc.port.SelectMeta(place.NoEnrichContext(ctx), nil)
	if err != nil {
		return nil, err
	}
	result := make(TagData)
	for _, m := range metas {
		if tl, ok := m.GetList(meta.KeyTags); ok && len(tl) > 0 {
			for _, t := range tl {
				result[t] = append(result[t], m)
			}
		}
	}
	if minCount > 1 {
		for t, ms := range result {
			if len(ms) < minCount {
				delete(result, t)
			}
		}
	}
	return result, nil
}

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// -------- List syntax ------------------------------------------------------

// ListSyntaxPort is the interface used by this use case.
type ListSyntaxPort interface {
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}

// ListSyntax is the data for this use case.
type ListSyntax struct {
	port ListSyntaxPort
}

// NewListSyntax creates a new use case.
func NewListSyntax(port ListSyntaxPort) ListSyntax {
	return ListSyntax{port: port}
}

// Run executes the use case.
func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) {
	q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
	if err != nil {
		return nil, err
	}
	result := meta.CreateArrangement(metas, api.KeySyntax)
	for _, syn := range parser.GetSyntaxes() {
		if _, found := result[syn]; !found {
			delete(result, syn)
		}
	}
	return result, nil
}

// -------- List roles -------------------------------------------------------

// ListRolesPort is the interface used by this use case.
type ListRolesPort interface {
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}

// ListRoles is the data for this use case.
type ListRoles struct {
	port ListRolesPort
}

// NewListRoles creates a new use case.
func NewListRoles(port ListRolesPort) ListRoles {
	return ListRoles{port: port}
}

// Run executes the use case.
func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) {
	q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
	if err != nil {
		return nil, err
	}
	return meta.CreateArrangement(metas, api.KeyRole), nil
}

Deleted usecase/new_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







































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/strfun"
)

// NewZettel is the data for this use case.
type NewZettel struct{}

// NewNewZettel creates a new use case.
func NewNewZettel() NewZettel {
	return NewZettel{}
}

// Run executes the use case.
func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel {
	m := origZettel.Meta.Clone()
	const prefix = "new-"
	for _, pair := range m.PairsRest(false) {
		if key := pair.Key; len(key) > len(prefix) && key[0:len(prefix)] == prefix {
			m.Set(key[len(prefix):], pair.Value)
			m.Delete(key)
		}
	}
	content := strfun.TrimSpaceRight(origZettel.Content.AsString())
	return domain.Zettel{Meta: m, Content: domain.Content(content)}
}

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
55























































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the Zettelstore.
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
	parseZettel ParseZettel
}

// NewZettelOrder creates a new use case.
func NewZettelOrder(port ZettelOrderPort, parseZettel ParseZettel) ZettelOrder {
	return ZettelOrder{port: port, parseZettel: parseZettel}
}

// 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.parseZettel.Run(ctx, zid, syntax)
	if err != nil {
		return nil, nil, err
	}
	for _, ref := range collect.Order(zn) {
		if zid, err := id.Parse(ref.URL.Path); err == nil {
			if m, err := uc.port.GetMeta(ctx, zid); err == 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
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

-
+

-
+




+
+
+


-







-
-
+
+














-
-
+





-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
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
}

// NewParseZettel creates a new use case.
func NewParseZettel(rtConfig config.Config, getZettel GetZettel) ParseZettel {
	return ParseZettel{rtConfig: rtConfig, getZettel: getZettel}
}

// Run executes the use case.
func (uc ParseZettel) Run(
	ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
func (uc ParseZettel) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
	zettel, err := uc.getZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}

	return parser.ParseZettel(zettel, syntax, uc.rtConfig), nil
	return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil
}

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
279
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// QueryPort is the interface used by this use case.
type QueryPort interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}

// Query is the data for this use case.
type Query struct {
	port       QueryPort
	ucEvaluate Evaluate
}

// NewQuery creates a new use case.
func NewQuery(port QueryPort) Query {
	return Query{port: port}
}

// SetEvaluate sets the usecase Evaluate, because of circular dependencies.
func (uc *Query) SetEvaluate(ucEvaluate *Evaluate) { uc.ucEvaluate = *ucEvaluate }

// Run executes the use case.
func (uc *Query) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	zids := q.GetZids()
	if zids == nil {
		return uc.port.SelectMeta(ctx, nil, q)
	}
	if len(zids) == 0 {
		return nil, nil
	}
	metaSeq, err := uc.getMetaZid(ctx, zids)
	if err != nil {
		return metaSeq, err
	}
	if metaSeq = uc.processDirectives(ctx, metaSeq, q.GetDirectives()); len(metaSeq) > 0 {
		return uc.port.SelectMeta(ctx, metaSeq, q)
	}
	return nil, nil
}

func (uc *Query) getMetaZid(ctx context.Context, zids []id.Zid) ([]*meta.Meta, error) {
	metaSeq := make([]*meta.Meta, 0, len(zids))
	for _, zid := range zids {
		m, err := uc.port.GetMeta(ctx, zid)
		if err == nil {
			metaSeq = append(metaSeq, m)
			continue
		}
		if errors.Is(err, &box.ErrNotAllowed{}) {
			continue
		}
		return metaSeq, err
	}
	return metaSeq, nil
}

func (uc *Query) processDirectives(ctx context.Context, metaSeq []*meta.Meta, directives []query.Directive) []*meta.Meta {
	if len(directives) == 0 {
		return metaSeq
	}
	for _, dir := range directives {
		if len(metaSeq) == 0 {
			return nil
		}
		switch ds := dir.(type) {
		case *query.ContextSpec:
			metaSeq = uc.processContextDirective(ctx, ds, metaSeq)
		case *query.IdentSpec:
			// Nothing to do.
		case *query.ItemsSpec:
			metaSeq = uc.processItemsDirective(ctx, ds, metaSeq)
		case *query.UnlinkedSpec:
			metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq)
		default:
			panic(fmt.Sprintf("Unknown directive %T", ds))
		}
	}
	if len(metaSeq) == 0 {
		return nil
	}
	return metaSeq
}
func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta {
	return spec.Execute(ctx, metaSeq, uc.port)
}

func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, 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.ContainsOrNil(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)...)
		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
}

Added usecase/refresh.go.












































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) 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/logger"
)

// RefreshPort is the interface used by this use case.
type RefreshPort interface {
	Refresh(context.Context) error
}

// Refresh is the data for this use case.
type Refresh struct {
	log  *logger.Logger
	port RefreshPort
}

// NewRefresh creates a new use case.
func NewRefresh(log *logger.Logger, port RefreshPort) Refresh {
	return Refresh{log: log, port: port}
}

// Run executes the use case.
func (uc *Refresh) Run(ctx context.Context) error {
	err := uc.port.Refresh(ctx)
	uc.log.Info().User(ctx).Err(err).Msg("Refresh internal data")
	return err
}

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
}

Changes to 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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18



19
20
21
22
23
24
25
26


27


28
29
30
31
32
33
34
35
36

37
38
39

40
41
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/box"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// RenameZettelPort is the interface used by this use case.
type RenameZettelPort interface {
	// 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)

	// 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 place operation.
// 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 {
func (err ErrZidInUse) Error() string {
	return "Zettel id already in use: " + err.Zid.String()
}

// NewRenameZettel creates a new use case.
func NewRenameZettel(port RenameZettelPort) RenameZettel {
	return RenameZettel{port: port}
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 := place.NoEnrichContext(ctx)
	if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil {
func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
	noEnrichCtx := box.NoEnrichContext(ctx)
	if _, err := uc.port.GetZettel(noEnrichCtx, curZid); err != nil {
		return err
	}
	if newZid == curZid {
		// Nothing to do
		return nil
	}
	if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil {
		return &ErrZidInUse{Zid: newZid}
	if _, err := uc.port.GetZettel(noEnrichCtx, newZid); err == nil {
		return ErrZidInUse{Zid: newZid}
	}
	return uc.port.RenameZettel(ctx, curZid, 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/search.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44












































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
)

// SearchPort is the interface used by this use case.
type SearchPort interface {
	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// Search is the data for this use case.
type Search struct {
	port SearchPort
}

// NewSearch creates a new use case.
func NewSearch(port SearchPort) Search {
	return Search{port: port}
}

// Run executes the use case.
func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
	if !s.HasComputedMetaKey() {
		ctx = place.NoEnrichContext(ctx)
	}
	return uc.port.SelectMeta(ctx, s)
}

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







53


54
55
56

57

58
59

60

61



62
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19





20
21
22
23
24
25
26
27
28
29

30
31
32

33
34
35
36
37
38
39
40
41
42


43
44
45
46
47

48
49

50
51
52
53
54
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/box"
	"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(port UpdateZettelPort) UpdateZettel {
	return UpdateZettel{port: port}
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(place.NoEnrichContext(ctx), m.Zid)
	oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid)
	if err != nil {
		return err
	}
	if zettel.Equal(oldZettel, false) {
		return nil
	}

	// Update relevant computed, but stored values.
	if _, found := m.Get(api.KeyCreated); !found {
		if val, crFound := oldZettel.Meta.Get(api.KeyCreated); crFound {
			m.Set(api.KeyCreated, val)
		}
	}
	m.SetNow(meta.KeyModified)
	m.SetNow(api.KeyModified)

	m.YamlSep = oldZettel.Meta.YamlSep
	if m.Zid == id.ConfigurationZid {
		m.Set(meta.KeySyntax, meta.ValueSyntaxNone)
		m.Set(api.KeySyntax, meta.SyntaxNone)
	}

	if !hasContent {
		zettel.Content = domain.Content(strfun.TrimSpaceRight(oldZettel.Content.AsString()))
		zettel.Content = oldZettel.Content
	}
	zettel.Content.TrimSpace()
	return uc.port.UpdateZettel(ctx, zettel)
	err = uc.port.UpdateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel")
	return err
}

Added usecase/usecase.go.
















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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

Added usecase/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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/z/kernel"
)

// Version is the data for this use case.
type Version struct {
	vr VersionResult
}

// NewVersion creates a new use case.
func NewVersion(version string) Version {
	return Version{calculateVersionResult(version)}
}

// VersionResult is the data structure returned by this usecase.
type VersionResult struct {
	Major int
	Minor int
	Patch int
	Info  string
	Hash  string
}

var invalidVersion = VersionResult{
	Major: -1,
	Minor: -1,
	Patch: -1,
	Info:  kernel.CoreDefaultVersion,
	Hash:  "",
}

var reVersion = regexp.MustCompile(`^(\d+)\.(\d+)(\.(\d+))?(-(([[:alnum:]]|-)+))?(\+(([[:alnum:]])+(-[[:alnum:]]+)?))?`)

func calculateVersionResult(version string) VersionResult {
	match := reVersion.FindStringSubmatch(version)
	if len(match) < 12 {
		return invalidVersion
	}
	major, err := strconv.Atoi(match[1])
	if err != nil {
		return invalidVersion
	}
	minor, err := strconv.Atoi(match[2])
	if err != nil {
		return invalidVersion
	}
	patch, err := strconv.Atoi(match[4])
	if err != nil {
		patch = 0
	}
	return VersionResult{
		Major: major,
		Minor: minor,
		Patch: patch,
		Info:  match[6],
		Hash:  match[9],
	}
}

// Run executes the use case.
func (uc Version) Run() VersionResult { return uc.vr }

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"

	"zettelstore.de/client.fossil/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
35
36
37
38




39
40
41
42
43

44
45
46
47

48
49
50
51
52
53
54

55
56
57


58
59
60
61



62







































1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


26
27
28
29
30
31
32
33
34
35
36

37
38
39

40
41
42
43
44
45


46
47
48
49
50
51
52

53
54
55
56
57

58
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) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"bytes"
	"context"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"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
	rtConfig config.Config
	authz    auth.AuthzManager
	token    auth.TokenManager
	rtConfig config.Config
	auth     server.Auth
	policy   auth.Policy

	tokenLifetime time.Duration
}

// New creates a new API object.
func New(b server.Builder, authz auth.AuthzManager, token auth.TokenManager, auth server.Auth, rtConfig config.Config) *API {
	api := &API{
func New(log *logger.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager,
	rtConfig config.Config, pol auth.Policy) *API {
	a := &API{
		log:      log,
		b:        b,
		authz:    authz,
		token:    token,
		auth:     auth,
		rtConfig: rtConfig,
		policy:   pol,

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration),
	}
	return api
	return a
}

// GetURLPrefix returns the configured URL prefix of the web server.
func (api *API) GetURLPrefix() string { return api.b.GetURLPrefix() }

// NewURLBuilder creates a new URL builder object with the given key.
func (api *API) NewURLBuilder(key byte) server.URLBuilder { return api.b.NewURLBuilder(key) }
func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) }

func (api *API) getAuthData(ctx context.Context) *server.AuthData {
	return api.auth.GetAuthData(ctx)
func (a *API) getAuthData(ctx context.Context) *server.AuthData {
	return server.GetAuthData(ctx)
}
func (api *API) withAuth() bool { return api.authz.WithAuth() }
func (api *API) getToken(ident *meta.Meta) ([]byte, error) {
	return api.token.GetToken(ident, api.tokenLifetime, auth.KindJSON)
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.KindAPI)
}

func (a *API) reportUsecaseError(w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		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.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
}

Added 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
50
51
52
53
54
55
56
57
58
59
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/usecase"
)

// MakePostCommandHandler creates a new HTTP handler to execute certain commands.
func (a *API) MakePostCommandHandler(
	ucIsAuth *usecase.IsAuthenticated,
	ucRefresh *usecase.Refresh,
) http.HandlerFunc {
	return 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:
		w.WriteHeader(http.StatusNoContent)
	case usecase.IsAuthenticatedAndInvalid:
		w.WriteHeader(http.StatusUnauthorized)
	default:
		http.Error(w, "Unexpected result value", http.StatusInternalServerError)
	}
}

Deleted web/adapter/api/content_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

























































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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

const plainText = "text/plain; charset=utf-8"

var mapFormat2CT = map[string]string{
	"html":   "text/html; charset=utf-8",
	"native": plainText,
	"json":   "application/json",
	"djson":  "application/json",
	"text":   plainText,
	"zmk":    plainText,
	"raw":    plainText, // In some cases...
}

func format2ContentType(format string) string {
	ct, ok := mapFormat2CT[format]
	if !ok {
		return "application/octet-stream"
	}
	return ct
}

var mapSyntax2CT = map[string]string{
	"css":      "text/css; charset=utf-8",
	"gif":      "image/gif",
	"html":     "text/html; charset=utf-8",
	"jpeg":     "image/jpeg",
	"jpg":      "image/jpeg",
	"js":       "text/javascript; charset=utf-8",
	"pdf":      "application/pdf",
	"png":      "image/png",
	"svg":      "image/svg+xml",
	"xml":      "text/xml; charset=utf-8",
	"zmk":      "text/x-zmk; charset=utf-8",
	"plain":    plainText,
	"text":     plainText,
	"markdown": "text/markdown; charset=utf-8",
	"md":       "text/markdown; charset=utf-8",
	"mustache": plainText,
	//"graphviz":      "text/vnd.graphviz; charset=utf-8",
}

func syntax2contentType(syntax string) (string, bool) {
	contentType, ok := mapSyntax2CT[syntax]
	return contentType, ok
}

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		enc, encStr := getEncoding(r, q)
		var zettel zettel.Zettel
		var err error
		switch enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, id.Invalid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, id.Invalid)
		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(newZid.ZettelID())
		switch enc {
		case api.EncoderPlain:
			result = newZid.Bytes()
			contentType = content.PlainText
		case api.EncoderData:
			result = []byte(sx.Int64(newZid).String())
			contentType = content.SXPF
		default:
			panic(encStr)
		}

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		if _, err = w.Write(result); err != nil {
			a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel")
		}
	}
}

Added 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
36
37
38
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package api

import (
	"net/http"

	"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) {
		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)
	}
}

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 (
	"net/http"

	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeGetDataHandler creates a new HTTP handler to return zettelstore data.
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		version := ucVersion.Run()
		err := a.writeObject(w, id.Invalid, sx.MakeList(
			sx.Int64(version.Major),
			sx.Int64(version.Minor),
			sx.Int64(version.Patch),
			sx.String(version.Info),
			sx.String(version.Hash),
		))
		if err != nil {
			a.log.Error().Err(err).Msg("Write Version Info")
		}
	}
}

Deleted web/adapter/api/get_links.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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


































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"encoding/json"
	"net/http"
	"strconv"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

type jsonGetLinks struct {
	ID    string `json:"id"`
	URL   string `json:"url"`
	Links struct {
		Incoming []jsonIDURL `json:"incoming"`
		Outgoing []jsonIDURL `json:"outgoing"`
		Local    []string    `json:"local"`
		External []string    `json:"external"`
	} `json:"links"`
	Images struct {
		Outgoing []jsonIDURL `json:"outgoing"`
		Local    []string    `json:"local"`
		External []string    `json:"external"`
	} `json:"images"`
	Cites []string `json:"cites"`
}

// MakeGetLinksHandler creates a new API handler to return links to other material.
func (api *API) MakeGetLinksHandler(parseZettel usecase.ParseZettel) 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()
		zn, err := parseZettel.Run(ctx, zid, q.Get("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		summary := collect.References(zn)

		kind := getKindFromValue(q.Get("kind"))
		matter := getMatterFromValue(q.Get("matter"))
		if !validKindMatter(kind, matter) {
			adapter.BadRequest(w, "Invalid kind/matter")
			return
		}

		outData := jsonGetLinks{
			ID:  zid.String(),
			URL: api.NewURLBuilder('z').SetZid(zid).String(),
		}
		if kind&kindLink != 0 {
			api.setupLinkJSONRefs(summary, matter, &outData)
		}
		if kind&kindImage != 0 {
			api.setupImageJSONRefs(summary, matter, &outData)
		}
		if kind&kindCite != 0 {
			outData.Cites = stringCites(summary.Cites)
		}

		w.Header().Set(adapter.ContentType, format2ContentType("json"))
		enc := json.NewEncoder(w)
		enc.SetEscapeHTML(false)
		enc.Encode(&outData)
	}
}

func (api *API) setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) {
	if matter&matterIncoming != 0 {
		outData.Links.Incoming = []jsonIDURL{}
	}
	zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links)
	if matter&matterOutgoing != 0 {
		outData.Links.Outgoing = api.idURLRefs(zetRefs)
	}
	if matter&matterLocal != 0 {
		outData.Links.Local = stringRefs(locRefs)
	}
	if matter&matterExternal != 0 {
		outData.Links.External = stringRefs(extRefs)
	}
}

func (api *API) setupImageJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) {
	zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Images)
	if matter&matterOutgoing != 0 {
		outData.Images.Outgoing = api.idURLRefs(zetRefs)
	}
	if matter&matterLocal != 0 {
		outData.Images.Local = stringRefs(locRefs)
	}
	if matter&matterExternal != 0 {
		outData.Images.External = stringRefs(extRefs)
	}
}

func (api *API) idURLRefs(refs []*ast.Reference) []jsonIDURL {
	result := make([]jsonIDURL, 0, len(refs))
	for _, ref := range refs {
		path := ref.URL.Path
		ub := api.NewURLBuilder('z').AppendPath(path)
		if fragment := ref.URL.Fragment; len(fragment) > 0 {
			ub.SetFragment(fragment)
		}
		result = append(result, jsonIDURL{ID: path, URL: ub.String()})
	}
	return result
}

func stringRefs(refs []*ast.Reference) []string {
	result := make([]string, 0, len(refs))
	for _, ref := range refs {
		result = append(result, ref.String())
	}
	return result
}

func stringCites(cites []*ast.CiteNode) []string {
	mapKey := make(map[string]bool)
	result := make([]string, 0, len(cites))
	for _, cn := range cites {
		if _, ok := mapKey[cn.Key]; !ok {
			mapKey[cn.Key] = true
			result = append(result, cn.Key)
		}
	}
	return result
}

type kindType int

const (
	_ kindType = 1 << iota
	kindLink
	kindImage
	kindCite
)

var mapKind = map[string]kindType{
	"":      kindLink | kindImage | kindCite,
	"link":  kindLink,
	"image": kindImage,
	"cite":  kindCite,
	"both":  kindLink | kindImage,
	"all":   kindLink | kindImage | kindCite,
}

func getKindFromValue(value string) kindType {
	if k, ok := mapKind[value]; ok {
		return k
	}
	if n, err := strconv.Atoi(value); err == nil && n > 0 {
		return kindType(n)
	}
	return 0
}

type matterType int

const (
	_ matterType = 1 << iota
	matterIncoming
	matterOutgoing
	matterLocal
	matterExternal
)

var mapMatter = map[string]matterType{
	"":         matterIncoming | matterOutgoing | matterLocal | matterExternal,
	"incoming": matterIncoming,
	"outgoing": matterOutgoing,
	"local":    matterLocal,
	"external": matterExternal,
	"zettel":   matterIncoming | matterOutgoing,
	"material": matterLocal | matterExternal,
	"all":      matterIncoming | matterOutgoing | matterLocal | matterExternal,
}

func getMatterFromValue(value string) matterType {
	if m, ok := mapMatter[value]; ok {
		return m
	}
	if n, err := strconv.Atoi(value); err == nil && n > 0 {
		return matterType(n)
	}
	return 0
}

func validKindMatter(kind kindType, matter matterType) bool {
	if kind == 0 {
		return false
	}
	if kind&kindLink != 0 {
		return matter != 0
	}
	if kind&kindImage != 0 {
		if matter == 0 || matter == matterIncoming {
			return false
		}
		return true
	}
	if kind&kindCite != 0 {
		return matter == matterOutgoing
	}
	return false
}

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








































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetOrderHandler creates a new API handler to return zettel references
// of a given zettel.
func (api *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("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		api.writeMetaList(w, start, metas)
	}
}

Deleted web/adapter/api/get_role_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
































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"fmt"
	"net/http"

	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/jsonenc"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel".
func (api *API) MakeListRoleHandler(listRole usecase.ListRole) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		roleList, err := listRole.Run(r.Context())
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case "json":
			w.Header().Set(adapter.ContentType, format2ContentType(format))
			renderListRoleJSON(w, roleList)
		default:
			adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", format))
		}

	}
}

func renderListRoleJSON(w http.ResponseWriter, roleList []string) {
	buf := encoder.NewBufWriter(w)

	buf.WriteString("{\"role-list\":[")
	first := true
	for _, role := range roleList {
		if first {
			buf.WriteByte('"')
			first = false
		} else {
			buf.WriteString("\",\"")
		}
		buf.Write(jsonenc.Escape(role))
	}
	if !first {
		buf.WriteByte('"')
	}
	buf.WriteString("]}")
	buf.Flush()
}

Deleted web/adapter/api/get_tags_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
















































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"fmt"
	"net/http"
	"sort"
	"strconv"

	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/jsonenc"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel".
func (api *API) MakeListTagsHandler(listTags usecase.ListTags) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min"))
		tagData, err := listTags.Run(r.Context(), iMinCount)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case "json":
			w.Header().Set(adapter.ContentType, format2ContentType(format))
			renderListTagsJSON(w, tagData)
		default:
			adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", format))
		}
	}
}

func renderListTagsJSON(w http.ResponseWriter, tagData usecase.TagData) {
	buf := encoder.NewBufWriter(w)

	tagList := make([]string, 0, len(tagData))
	for tag := range tagData {
		tagList = append(tagList, tag)
	}
	sort.Strings(tagList)

	buf.WriteString("{\"tags\":{")
	first := true
	for _, tag := range tagList {
		if first {
			buf.WriteByte('"')
			first = false
		} else {
			buf.WriteString(",\"")
		}
		buf.Write(jsonenc.Escape(tag))
		buf.WriteString("\":[")
		for i, meta := range tagData[tag] {
			if i > 0 {
				buf.WriteByte(',')
			}
			buf.WriteByte('"')
			buf.WriteString(meta.Zid.String())
			buf.WriteByte('"')
		}
		buf.WriteString("]")

	}
	buf.WriteString("}}")
	buf.Flush()
}

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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16

17
18
19
20
21




22
23
24
25
26
27

28
29
30
31
32
33
34
35


36
37
38
39
40
41
42
43
44
45
46
47



48
49
50
51








52
53
54
55
56
57
58
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

-
+

-
+




+
+
+


-



-
+
+



-
-
-
-
+
+
+
+
+

-
+


+
+
+


-
-
+
+







+
+

-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
+

-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
-
-
+
+

-

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"errors"
	"bytes"
	"context"
	"fmt"
	"net/http"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/place"
	"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 rendered zettel.
func (api *API) MakeGetZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc {
// MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings.
func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()
		part := getPart(q, partContent)
		ctx := r.Context()
		q := r.URL.Query()
		format := adapter.GetFormat(r, q, encoder.GetDefaultFormat())
		if format == "raw" {
		switch enc, encStr := getEncoding(r, q); enc {
		case api.EncoderPlain:
			a.writePlainData(w, ctx, zid, part, getZettel)

			ctx = place.NoEnrichContext(ctx)
		}
		zn, err := parseZettel.Run(ctx, zid, q.Get("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		case api.EncoderData:
			a.writeSzData(w, ctx, zid, part, getZettel)

		default:
			var zn *ast.ZettelNode
			var em func(value string) ast.InlineSlice
			if q.Has(api.QueryKeyParseOnly) {
				zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
				em = parser.ParseMetadata
			} 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(ctx, w, zn, em, enc, encStr, part)
		}
		part := getPart(q, partZettel)
		if part == partUnknown {
			adapter.BadRequest(w, "Unknown _part parameter")
			return
		}
		switch format {
	}
}

		case "json", "djson":
			w.Header().Set(adapter.ContentType, format2ContentType(format))
func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var contentType string
			err = api.getWriteMetaZettelFunc(ctx, format, part, partZettel, getMeta)(w, zn)
			if err != nil {
	var err error
				adapter.InternalServerError(w, "Write D/JSON", err)
			}
			return
		}

	z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
	if err != nil {
		a.reportUsecaseError(w, err)
		return
	}

		env := encoder.Environment{
			LinkAdapter:    adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, part.DefString(partZettel), format),
			ImageAdapter:   adapter.MakeImageAdapter(ctx, api, getMeta),
			CiteAdapter:    nil,
			Lang:           config.GetLang(zn.InhMeta, api.rtConfig),
			Xhtml:          false,
			MarkerExternal: "",
			NewWindow:      false,
			IgnoreMeta:     map[string]bool{meta.KeyLang: true},
		}
		switch part {
		case partZettel:
			err = writeZettelPartZettel(w, zn, format, env)
		case partMeta:
			err = writeZettelPartMeta(w, zn, format)
		case partContent:
			err = api.writeZettelPartContent(w, zn, format, env)
		}
		if err != nil {
			if errors.Is(err, adapter.ErrNoSuchFormat) {
	switch part {
	case partZettel:
		_, err = z.Meta.Write(&buf)
		if err == nil {
			err = buf.WriteByte('\n')
		}
		if err == nil {
			_, err = z.Content.Write(&buf)
		}

	case partMeta:
		contentType = content.PlainText
		_, err = z.Meta.Write(&buf)

	case partContent:
		contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, 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 {
		a.log.Error().Err(err).Zid(zid).Msg("Write Plain data")
	}
}

func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)
				adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in format %q", zid.String(), format))
				return
			}
		return
	}
			adapter.InternalServerError(w, "Get zettel", err)
		}
	}
}

	var obj sx.Object
	switch part {
	case partZettel:
		zContent, zEncoding := z.Content.Encode()
		obj = sexp.EncodeZettel(api.ZettelData{
			Meta:     z.Meta.Map(),
			Rights:   a.getRights(ctx, z.Meta),
			Encoding: zEncoding,
			Content:  zContent,
		})

	case partMeta:
		obj = sexp.EncodeMetaRights(api.MetaRights{
			Meta:   z.Meta.Map(),
			Rights: a.getRights(ctx, z.Meta),
		})
	}
func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format string, env encoder.Environment) error {
	enc := encoder.Create(format, &env)
	if enc == nil {
	if err = a.writeObject(w, zid, obj); err != nil {
		return adapter.ErrNoSuchFormat
		a.log.Error().Err(err).Zid(zid).Msg("write sx data")
	}
	inhMeta := false
	if format != "raw" {
		w.Header().Set(adapter.ContentType, format2ContentType(format))
		inhMeta = true
	}
}
	_, err := enc.WriteZettel(w, zn, inhMeta)
	return err
}

func writeZettelPartMeta(w http.ResponseWriter, zn *ast.ZettelNode, format string) error {
	w.Header().Set(adapter.ContentType, format2ContentType(format))
	if enc := encoder.Create(format, nil); enc != nil {
		if format == "raw" {
			_, err := enc.WriteMeta(w, zn.Meta)
			return err
		}
		_, err := enc.WriteMeta(w, zn.InhMeta)
		return err

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,
		&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.Error().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	return adapter.ErrNoSuchFormat
}

		return
	}
	if buf.Len() == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}
func (api *API) writeZettelPartContent(w http.ResponseWriter, zn *ast.ZettelNode, format string, env encoder.Environment) error {
	if format == "raw" {
		if ct, ok := syntax2contentType(config.GetSyntax(zn.Meta, api.rtConfig)); ok {
			w.Header().Add(adapter.ContentType, ct)
		}

	} else {
		w.Header().Set(adapter.ContentType, format2ContentType(format))
	if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil {
		a.log.Error().Err(err).Zid(zn.Zid).Msg("Write Encoded Zettel")
	}
	return writeContent(w, zn, format, &env)
}

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
















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	"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 (api *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 := usecase.ParseZCDirection(q.Get("dir"))
		depth, ok := adapter.GetInteger(q, "depth")
		if !ok || depth < 0 {
			depth = 5
		}
		limit, ok := adapter.GetInteger(q, "limit")
		if !ok || limit < 0 {
			limit = 200
		}
		ctx := r.Context()
		metaList, err := getContext.Run(ctx, zid, dir, depth, limit)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		api.writeMetaList(w, metaList[0], metaList[1:])
	}
}

Deleted web/adapter/api/get_zettel_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






















































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"fmt"
	"net/http"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel".
func (api *API) MakeListMetaHandler(
	listMeta usecase.ListMeta,
	getMeta usecase.GetMeta,
	parseZettel usecase.ParseZettel,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		s := adapter.GetSearch(q, false)
		format := adapter.GetFormat(r, q, encoder.GetDefaultFormat())
		part := getPart(q, partMeta)
		if part == partUnknown {
			adapter.BadRequest(w, "Unknown _part parameter")
			return
		}
		ctx1 := ctx
		if format == "html" || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) {
			ctx1 = place.NoEnrichContext(ctx1)
		}
		metaList, err := listMeta.Run(ctx1, s)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		w.Header().Set(adapter.ContentType, format2ContentType(format))
		switch format {
		case "html":
			api.renderListMetaHTML(w, metaList)
		case "json", "djson":
			api.renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel)
		case "native", "raw", "text", "zmk":
			adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", format))
		default:
			adapter.BadRequest(w, fmt.Sprintf("Zettel list not available in format %q", format))
		}
	}
}

func (api *API) renderListMetaHTML(w http.ResponseWriter, metaList []*meta.Meta) {
	env := encoder.Environment{Interactive: true}
	buf := encoder.NewBufWriter(w)
	buf.WriteStrings("<html lang=\"", api.rtConfig.GetDefaultLang(), "\">\n<body>\n<ul>\n")
	for _, m := range metaList {
		title := m.GetDefault(meta.KeyTitle, "")
		htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env)
		if err != nil {
			adapter.InternalServerError(w, "Format HTML inlines", err)
			return
		}
		buf.WriteStrings(
			"<li><a href=\"",
			api.NewURLBuilder('z').SetZid(m.Zid).AppendQuery("_format", "html").String(),
			"\">",
			htmlTitle,
			"</a></li>\n")
	}
	buf.WriteString("</ul>\n</body>\n</html>")
	buf.Flush()
}

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
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






















































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"context"
	"encoding/json"
	"io"
	"net/http"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

type jsonIDURL struct {
	ID  string `json:"id"`
	URL string `json:"url"`
}
type jsonZettel struct {
	ID       string            `json:"id"`
	URL      string            `json:"url"`
	Meta     map[string]string `json:"meta"`
	Encoding string            `json:"encoding"`
	Content  interface{}       `json:"content"`
}
type jsonMeta struct {
	ID   string            `json:"id"`
	URL  string            `json:"url"`
	Meta map[string]string `json:"meta"`
}
type jsonMetaList struct {
	ID   string            `json:"id"`
	URL  string            `json:"url"`
	Meta map[string]string `json:"meta"`
	List []jsonMeta        `json:"list"`
}
type jsonContent struct {
	ID       string      `json:"id"`
	URL      string      `json:"url"`
	Encoding string      `json:"encoding"`
	Content  interface{} `json:"content"`
}

func encodedContent(content domain.Content) (string, interface{}) {
	if content.IsBinary() {
		return "base64", content.AsBytes()
	}
	return "", content.AsString()
}

var (
	djsonMetaHeader    = []byte(",\"meta\":")
	djsonContentHeader = []byte(",\"content\":")
	djsonHeader1       = []byte("{\"id\":\"")
	djsonHeader2       = []byte("\",\"url\":\"")
	djsonHeader3       = []byte("?_format=")
	djsonHeader4       = []byte("\"")
	djsonFooter        = []byte("}")
)

func (api *API) writeDJSONHeader(w io.Writer, zid id.Zid) error {
	_, err := w.Write(djsonHeader1)
	if err == nil {
		_, err = w.Write(zid.Bytes())
	}
	if err == nil {
		_, err = w.Write(djsonHeader2)
	}
	if err == nil {
		_, err = io.WriteString(w, api.NewURLBuilder('z').SetZid(zid).String())
	}
	if err == nil {
		_, err = w.Write(djsonHeader3)
		if err == nil {
			_, err = io.WriteString(w, "djson")
		}
	}
	if err == nil {
		_, err = w.Write(djsonHeader4)
	}
	return err
}

func (api *API) renderListMetaXJSON(
	ctx context.Context,
	w http.ResponseWriter,
	metaList []*meta.Meta,
	format string,
	part, defPart partType,
	getMeta usecase.GetMeta,
	parseZettel usecase.ParseZettel,
) {
	prepareZettel := api.getPrepareZettelFunc(ctx, parseZettel, part)
	writeZettel := api.getWriteMetaZettelFunc(ctx, format, part, defPart, getMeta)
	err := writeListXJSON(w, metaList, prepareZettel, writeZettel)
	if err != nil {
		adapter.InternalServerError(w, "Get list", err)
	}
}

type prepareZettelFunc func(m *meta.Meta) (*ast.ZettelNode, error)

func (api *API) getPrepareZettelFunc(ctx context.Context, parseZettel usecase.ParseZettel, part partType) prepareZettelFunc {
	switch part {
	case partZettel, partContent:
		return func(m *meta.Meta) (*ast.ZettelNode, error) {
			return parseZettel.Run(ctx, m.Zid, "")
		}
	case partMeta, partID:
		return func(m *meta.Meta) (*ast.ZettelNode, error) {
			return &ast.ZettelNode{
				Meta:    m,
				Content: "",
				Zid:     m.Zid,
				InhMeta: api.rtConfig.AddDefaultValues(m),
				Ast:     nil,
			}, nil
		}
	}
	return nil
}

type writeZettelFunc func(io.Writer, *ast.ZettelNode) error

func (api *API) getWriteMetaZettelFunc(ctx context.Context, format string,
	part, defPart partType, getMeta usecase.GetMeta) writeZettelFunc {
	switch part {
	case partZettel:
		return api.getWriteZettelFunc(ctx, format, defPart, getMeta)
	case partMeta:
		return api.getWriteMetaFunc(ctx, format)
	case partContent:
		return api.getWriteContentFunc(ctx, format, defPart, getMeta)
	case partID:
		return api.getWriteIDFunc(ctx, format)
	default:
		panic(part)
	}
}

func (api *API) getWriteZettelFunc(ctx context.Context, format string,
	defPart partType, getMeta usecase.GetMeta) writeZettelFunc {
	if format == "json" {
		return func(w io.Writer, zn *ast.ZettelNode) error {
			encoding, content := encodedContent(zn.Content)
			return encodeJSONData(w, jsonZettel{
				ID:       zn.Zid.String(),
				URL:      api.NewURLBuilder('z').SetZid(zn.Zid).String(),
				Meta:     zn.InhMeta.Map(),
				Encoding: encoding,
				Content:  content,
			})
		}
	}
	enc := encoder.Create("djson", nil)
	if enc == nil {
		panic("no DJSON encoder found")
	}
	return func(w io.Writer, zn *ast.ZettelNode) error {
		err := api.writeDJSONHeader(w, zn.Zid)
		if err != nil {
			return err
		}
		_, err = w.Write(djsonMetaHeader)
		if err != nil {
			return err
		}
		_, err = enc.WriteMeta(w, zn.InhMeta)
		if err != nil {
			return err
		}
		_, err = w.Write(djsonContentHeader)
		if err != nil {
			return err
		}
		err = writeContent(w, zn, "djson", &encoder.Environment{
			LinkAdapter:  adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, partZettel.DefString(defPart), "djson"),
			ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)})
		if err != nil {
			return err
		}
		_, err = w.Write(djsonFooter)
		return err
	}
}
func (api *API) getWriteMetaFunc(ctx context.Context, format string) writeZettelFunc {
	if format == "json" {
		return func(w io.Writer, zn *ast.ZettelNode) error {
			return encodeJSONData(w, jsonMeta{
				ID:   zn.Zid.String(),
				URL:  api.NewURLBuilder('z').SetZid(zn.Zid).String(),
				Meta: zn.InhMeta.Map(),
			})
		}
	}
	enc := encoder.Create("djson", nil)
	if enc == nil {
		panic("no DJSON encoder found")
	}
	return func(w io.Writer, zn *ast.ZettelNode) error {
		err := api.writeDJSONHeader(w, zn.Zid)
		if err != nil {
			return err
		}
		_, err = w.Write(djsonMetaHeader)
		if err != nil {
			return err
		}
		_, err = enc.WriteMeta(w, zn.InhMeta)
		if err != nil {
			return err
		}
		_, err = w.Write(djsonFooter)
		return err
	}
}
func (api *API) getWriteContentFunc(ctx context.Context, format string,
	defPart partType, getMeta usecase.GetMeta) writeZettelFunc {
	if format == "json" {
		return func(w io.Writer, zn *ast.ZettelNode) error {
			encoding, content := encodedContent(zn.Content)
			return encodeJSONData(w, jsonContent{
				ID:       zn.Zid.String(),
				URL:      api.NewURLBuilder('z').SetZid(zn.Zid).String(),
				Encoding: encoding,
				Content:  content,
			})
		}
	}
	return func(w io.Writer, zn *ast.ZettelNode) error {
		err := api.writeDJSONHeader(w, zn.Zid)
		if err != nil {
			return err
		}
		_, err = w.Write(djsonContentHeader)
		if err != nil {
			return err
		}
		err = writeContent(w, zn, "djson", &encoder.Environment{
			LinkAdapter:  adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, partContent.DefString(defPart), "djson"),
			ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)})
		if err != nil {
			return err
		}
		_, err = w.Write(djsonFooter)
		return err
	}
}
func (api *API) getWriteIDFunc(ctx context.Context, format string) writeZettelFunc {
	if format == "json" {
		return func(w io.Writer, zn *ast.ZettelNode) error {
			return encodeJSONData(w, jsonIDURL{
				ID:  zn.Zid.String(),
				URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(),
			})
		}
	}
	return func(w io.Writer, zn *ast.ZettelNode) error {
		err := api.writeDJSONHeader(w, zn.Zid)
		if err != nil {
			return err
		}
		_, err = w.Write(djsonFooter)
		return err
	}
}

var (
	jsonListHeader = []byte("{\"list\":[")
	jsonListSep    = []byte{','}
	jsonListFooter = []byte("]}")
)

func writeListXJSON(w http.ResponseWriter, metaList []*meta.Meta, prepareZettel prepareZettelFunc, writeZettel writeZettelFunc) error {
	_, err := w.Write(jsonListHeader)
	for i, m := range metaList {
		if err != nil {
			return err
		}
		if i > 0 {
			_, err = w.Write(jsonListSep)
			if err != nil {
				return err
			}
		}
		zn, err1 := prepareZettel(m)
		if err1 != nil {
			return err1
		}
		err = writeZettel(w, zn)
	}
	if err == nil {
		_, err = w.Write(jsonListFooter)
	}
	return err
}

func writeContent(w io.Writer, zn *ast.ZettelNode, format string, env *encoder.Environment) error {
	enc := encoder.Create(format, env)
	if enc == nil {
		return adapter.ErrNoSuchFormat
	}

	_, err := enc.WriteContent(w, zn)
	return err
}

func encodeJSONData(w io.Writer, data interface{}) error {
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)
	return enc.Encode(data)
}

func (api *API) writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error {
	outData := jsonMetaList{
		ID:   m.Zid.String(),
		URL:  api.NewURLBuilder('z').SetZid(m.Zid).String(),
		Meta: m.Map(),
		List: make([]jsonMeta, len(metaList)),
	}
	for i, m := range metaList {
		outData.List[i].ID = m.Zid.String()
		outData.List[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String()
		outData.List[i].Meta = m.Map()
	}
	w.Header().Set(adapter.ContentType, format2ContentType("json"))
	return encodeJSONData(w, outData)
}

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













1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16

17
18
19
20
21
22
23
24
25
26


27
28
29

30


31
32
33
34
35
36
37
38

39
40

41
42
43
44
45
46
47
48
49
50


51
52
53
54
55
56
57
58
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"encoding/json"
	"net/http"
	"time"

	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API.
func (api *API) MakePostLoginHandlerAPI(ucAuth usecase.Authenticate) http.HandlerFunc {
// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !api.withAuth() {
		if !a.withAuth() {
			w.Header().Set(adapter.ContentType, format2ContentType("json"))
			writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
			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(), ident, cred, api.tokenLifetime, auth.KindJSON)
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI)
			if err != nil {
				adapter.ReportUsecaseError(w, err)
				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
		}

		w.Header().Set(adapter.ContentType, format2ContentType("json"))
		writeJSONToken(w, string(token), api.tokenLifetime)
		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 writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) {
	je := json.NewEncoder(w)
	je.Encode(struct {
		Token   string `json:"access_token"`
		Type    string `json:"token_type"`
		Expires int    `json:"expires_in"`
	}{
		Token:   token,
		Type:    "Bearer",
		Expires: int(lifetime / time.Second),
	})
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (api *API) MakeRenewAuthHandler() http.HandlerFunc {
func (a *API) MakeRenewAuthHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Refresh/free")
			}
			return
		}
		authData := api.getAuthData(ctx)
		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 {
			w.Header().Set(adapter.ContentType, format2ContentType("json"))
			writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime)
			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 := api.getToken(authData.User)
		token, err := a.getToken(authData.User)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			a.reportUsecaseError(w, err)
			return
		}
		w.Header().Set(adapter.ContentType, format2ContentType("json"))
		writeJSONToken(w, string(token), api.tokenLifetime)
	}
}
		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.String("Bearer"),
		sx.String(token),
		sx.Int64(int64(lifetime/time.Second)),
	))
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		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, urlQuery); enc {
		case api.EncoderPlain:
			encoder = &plainZettelEncoder{}
			contentType = content.PlainText

		case api.EncoderData:
			encoder = &dataZettelEncoder{
				sq:        sq,
				getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) },
			}
			contentType = content.SXPF

		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		var buf bytes.Buffer
		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)
		}

		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, actions []string) error {
	min, max := -1, -1
	if len(actions) > 0 {
		acts := make([]string, 0, len(actions))
		for _, act := range actions {
			if strings.HasPrefix(act, api.MinAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					min = num
					continue
				}
			}
			if strings.HasPrefix(act, api.MaxAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					max = num
					continue
				}
			}
			acts = append(acts, act)
		}
		for _, act := range acts {
			if act == api.KeysAction {
				return encodeKeysArrangement(w, enc, ml, act)
			}
			switch key := strings.ToLower(act); meta.Type(key) {
			case meta.TypeWord, meta.TypeTagSet:
				return encodeMetaKeyArrangement(w, enc, ml, key, min, max)
			}
		}
	}
	return enc.writeMetaList(w, ml)
}

func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error {
	arr := make(meta.Arrangement, 128)
	for _, m := range ml {
		for k := range m.Map() {
			arr[k] = append(arr[k], m)
		}
	}
	return enc.writeArrangement(w, act, arr)
}

func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error {
	arr0 := meta.CreateArrangement(ml, key)
	arr := make(meta.Arrangement, len(arr0))
	for k0, ml0 := range arr0 {
		if len(ml0) < min || (max > 0 && len(ml0) > max) {
			continue
		}
		arr[k0] = ml0
	}
	return enc.writeArrangement(w, key, arr)
}

type zettelEncoder interface {
	writeMetaList(w io.Writer, ml []*meta.Meta) error
	writeArrangement(w io.Writer, act string, arr meta.Arrangement) error
}

type plainZettelEncoder struct{}

func (*plainZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
	for _, m := range ml {
		_, err := fmt.Fprintln(w, m.Zid.String(), m.GetTitle())
		if err != nil {
			return err
		}
	}
	return nil
}
func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error {
	for key, ml := range arr {
		_, err := io.WriteString(w, key)
		if err != nil {
			return err
		}
		for i, m := range ml {
			if i == 0 {
				_, err = io.WriteString(w, "\t")
			} else {
				_, err = io.WriteString(w, " ")
			}
			if err != nil {
				return err
			}
			_, err = io.WriteString(w, m.Zid.String())
			if err != nil {
				return err
			}
		}
		_, err = io.WriteString(w, "\n")
		if err != nil {
			return err
		}
	}
	return nil
}

type dataZettelEncoder struct {
	sq        *query.Query
	getRights func(*meta.Meta) api.ZettelRights
}

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{
			Meta:   m.Map(),
			Rights: dze.getRights(m),
		})
		msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel)
		result[i+1] = msz
	}

	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("meta-list"),
		sx.MakeList(sx.MakeSymbol("query"), sx.String(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.String(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.String(aggKey))
		result = result.Cons(sxMeta)
	}
	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("aggregate"),
		sx.String(act),
		sx.MakeList(sx.MakeSymbol("query"), sx.String(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.String(dze.sq.Human())),
		result.Cons(sx.SymbolList),
	))
	return err
}

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)
		}
	}
	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")
	}
}

Added 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
68
69
70
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"net/url"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeRenameZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		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
30



31
32
33
34
35
36
37
38

39
40
41

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
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) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import "net/url"
import (
	"io"
	"net/http"
	"net/url"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil/sxreader"
	"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
	}
	if enc, ok := getOneEncoding(r, api.HeaderAccept); ok {
		return api.Encoder(enc), enc
	}
	if enc, ok := getOneEncoding(r, api.HeaderContentType); ok {
		return api.Encoder(enc), enc
	}
	return api.EncoderPlain, api.EncoderPlain.String()
}

func getOneEncoding(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {
		for _, value := range values {
			if enc, ok2 := contentType2encoding(value); ok2 {
				return enc, true
			}
		}
	}
	return "", false
}

var mapCT2encoding = map[string]string{
	"text/html": api.EncodingHTML,
}

func contentType2encoding(contentType string) (string, bool) {
	// TODO: only check before first ';'
	enc, ok := mapCT2encoding[contentType]
	return enc, ok
}

type partType int

const (
	partUnknown partType = iota
	_ partType = iota
	partID
	partMeta
	partContent
	partZettel
)

var partMap = map[string]partType{
	"id":      partID,
	"meta":    partMeta,
	"content": partContent,
	"zettel":  partZettel,
	api.PartMeta:    partMeta,
	api.PartContent: partContent,
	api.PartZettel:  partZettel,
}

func getPart(q url.Values, defPart partType) partType {
	p := q.Get("_part")
	if p == "" {
		return defPart
	}
	if part, ok := partMap[p]; ok {
	if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok {
		return part
	}
	return partUnknown
	return defPart
}

func (p partType) String() string {
	switch p {
	case partID:
		return "id"
	case partMeta:
		return "meta"
	case partContent:
		return "content"
	case partZettel:
		return "zettel"
	case partUnknown:
		return "unknown"
	}
	return ""
}

func (p partType) DefString(defPart partType) string {
	if p == defPart {
		return ""
	}
	return p.String()
}

func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	defer r.Body.Close()
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return zettel.Zettel{}, err
	}
	inp := input.NewInput(b)
	m := meta.NewFromInput(zid, inp)
	return zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent(inp.Src[inp.Pos:]),
	}, nil
}

func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	defer r.Body.Close()
	rdr := sxreader.MakeReader(r.Body)
	obj, err := rdr.Read()
	if err != nil {
		return zettel.Zettel{}, err
	}
	zd, err := sexp.ParseZettel(obj)
	if err != nil {
		return zettel.Zettel{}, err
	}

	m := meta.New(zid)
	for k, v := range zd.Meta {
		if !meta.IsComputed(k) {
			m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
		}
	}

	var content zettel.Content
	if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil {
		return zettel.Zettel{}, err
	}

	return zettel.Zettel{
		Meta:    m,
		Content: content,
	}, nil
}

Added web/adapter/api/response.go.








































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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"

	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
)

func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, obj); err != nil {
		msg := a.log.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)
}

Added 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
55
56
57
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()
		var zettel zettel.Zettel
		switch enc, _ := getEncoding(r, q); enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, zid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, zid)
		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)
	}
}

Deleted web/adapter/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
54
55
56
57
58
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








































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package adapter provides handlers for web requests.
package adapter

import (
	"context"
	"errors"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
)

// ErrNoSuchFormat signals an unsupported encoding format
var ErrNoSuchFormat = errors.New("no such format")

// FormatInlines returns a string representation of the inline slice.
func FormatInlines(is ast.InlineSlice, format string, env *encoder.Environment) (string, error) {
	enc := encoder.Create(format, env)
	if enc == nil {
		return "", ErrNoSuchFormat
	}

	var content strings.Builder
	_, err := enc.WriteInlines(&content, is)
	if err != nil {
		return "", err
	}
	return content.String(), nil
}

// MakeLinkAdapter creates an adapter to change a link node during encoding.
func MakeLinkAdapter(
	ctx context.Context,
	b server.Builder,
	key byte,
	getMeta usecase.GetMeta,
	part, format string,
) func(*ast.LinkNode) ast.InlineNode {
	return func(origLink *ast.LinkNode) ast.InlineNode {
		origRef := origLink.Ref
		if origRef == nil {
			return origLink
		}
		if origRef.State == ast.RefStateBased {
			newLink := *origLink
			urlPrefix := b.GetURLPrefix()
			newRef := ast.ParseReference(urlPrefix + origRef.Value[1:])
			newRef.State = ast.RefStateHosted
			newLink.Ref = newRef
			return &newLink
		}
		if origRef.State != ast.RefStateZettel {
			return origLink
		}
		zid, err := id.Parse(origRef.URL.Path)
		if err != nil {
			panic(err)
		}
		_, err = getMeta.Run(place.NoEnrichContext(ctx), zid)
		if errors.Is(err, &place.ErrNotAllowed{}) {
			return &ast.FormatNode{
				Code:    ast.FormatSpan,
				Attrs:   origLink.Attrs,
				Inlines: origLink.Inlines,
			}
		}
		var newRef *ast.Reference
		if err == nil {
			ub := b.NewURLBuilder(key).SetZid(zid)
			if part != "" {
				ub.AppendQuery("_part", part)
			}
			if format != "" {
				ub.AppendQuery("_format", format)
			}
			if fragment := origRef.URL.EscapedFragment(); fragment != "" {
				ub.SetFragment(fragment)
			}

			newRef = ast.ParseReference(ub.String())
			newRef.State = ast.RefStateFound
		} else {
			newRef = ast.ParseReference(origRef.Value)
			newRef.State = ast.RefStateBroken
		}
		newLink := *origLink
		newLink.Ref = newRef
		return &newLink
	}
}

// MakeImageAdapter creates an adapter to change an image node during encoding.
func MakeImageAdapter(ctx context.Context, b server.Builder, getMeta usecase.GetMeta) func(*ast.ImageNode) ast.InlineNode {
	return func(origImage *ast.ImageNode) ast.InlineNode {
		if origImage.Ref == nil {
			return origImage
		}
		switch origImage.Ref.State {
		case ast.RefStateInvalid:
			return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateInvalid)
		case ast.RefStateZettel:
			zid, err := id.Parse(origImage.Ref.Value)
			if err != nil {
				panic(err)
			}
			_, err = getMeta.Run(place.NoEnrichContext(ctx), zid)
			if err != nil {
				return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateBroken)
			}
			return createZettelImage(b, origImage, zid, ast.RefStateFound)
		}
		return origImage
	}
}

func createZettelImage(b server.Builder, origImage *ast.ImageNode, zid id.Zid, state ast.RefState) *ast.ImageNode {
	newImage := *origImage
	newImage.Ref = ast.ParseReference(
		b.NewURLBuilder('z').SetZid(zid).AppendQuery("_part", "content").AppendQuery("_format", "raw").String())
	newImage.Ref.State = state
	return &newImage
}

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
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






-
+

-
+




+
+
+


-


-
+
-
-
-






-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
-
-
+
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package adapter provides handlers for web requests.
package adapter

import (
import "net/http"
	"log"
	"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)
}

// ErrResourceNotFound is signalled when a web resource was not found.
// NotFound signals HTTP status code 404.
func NotFound(w http.ResponseWriter, text string) {
	http.Error(w, text, http.StatusNotFound)
}

type ErrResourceNotFound struct{ Path string }
// InternalServerError signals HTTP status code 500.
func InternalServerError(w http.ResponseWriter, text string, err error) {
	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	if text == "" {
		log.Println(err)
	} else {
		log.Printf("%v: %v", text, err)
	}

}

func (ernf ErrResourceNotFound) Error() string { return "resource not found: " + ernf.Path }
// NotImplemented signals HTTP status code 501
func NotImplemented(w http.ResponseWriter, text string) {
	http.Error(w, text, http.StatusNotImplemented)
	log.Println(text)
}

Deleted web/adapter/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



















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package adapter provides handlers for web requests.
package adapter

import (
	"fmt"
	"log"
	"net/http"
	"strings"

	"zettelstore.de/z/encoder"
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user.
func MakePostLoginHandler(apiHandler, htmlHandler http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		switch format := GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()); format {
		case "json":
			apiHandler(w, r)
		case "html":
			htmlHandler(w, r)
		default:
			BadRequest(w, fmt.Sprintf("Authentication not available in format %q", format))
		}
	}
}

// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()
	if err != nil {
		log.Println(err)
		return "", "", false
	}

	ident = strings.TrimSpace(r.PostFormValue("username"))
	cred = r.PostFormValue("password")
	if ident == "" {
		return "", "", false
	}
	return ident, cred, true
}

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
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22


23
24
25
26



27
28
29








30



31





32









33
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package adapter provides handlers for web requests.
package adapter

import (
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
)

// GetInteger returns the integer value of the named query key.
func GetInteger(q url.Values, key string) (int, bool) {
	s := q.Get(key)
// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()
	if s != "" {
		if val, err := strconv.Atoi(s); err == nil {
			return val, true
		}
	}
	return 0, false
}

	if err != nil {
// ContentType defines the HTTP header value "Content-Type".
const ContentType = "Content-Type"

		kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form")
// GetFormat returns the data format selected by the caller.
func GetFormat(r *http.Request, q url.Values, defFormat string) string {
	format := q.Get("_format")
	if len(format) > 0 {
		return format
		return "", "", false
	}
	if format, ok := getOneFormat(r, "Accept"); ok {
		return format
	}
	if format, ok := getOneFormat(r, ContentType); ok {
		return format
	}
	return defFormat
}
	}

func getOneFormat(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {
		for _, value := range values {
	ident = strings.TrimSpace(r.PostFormValue("username"))
	cred = r.PostFormValue("password")
	if ident == "" {
			if format, ok := contentType2format(value); ok {
				return format, true
			}
		}
	}
	return "", false
}
		return "", "", false
	}

var mapCT2format = map[string]string{
	"application/json": "json",
	"text/html":        "html",
}

func contentType2format(contentType string) (string, bool) {
	// TODO: only check before first ';'
	format, ok := mapCT2format[contentType]
	return format, ok
	return ident, cred, true
}

// GetSearch retrieves the specified filter and sorting options from a query.
func GetSearch(q url.Values, forSearch bool) (s *search.Search) {
	sortQKey, orderQKey, offsetQKey, limitQKey, negateQKey, sQKey := getQueryKeys(forSearch)
	for key, values := range q {
		switch key {
		case sortQKey, orderQKey:
			s = extractOrderFromQuery(values, s)
		case offsetQKey:
			s = extractOffsetFromQuery(values, s)
		case limitQKey:
			s = extractLimitFromQuery(values, s)
		case negateQKey:
			s = s.SetNegate()
		case sQKey:
			s = setCleanedQueryValues(s, "", values)
		default:
			if !forSearch && meta.KeyIsValid(key) {
				s = setCleanedQueryValues(s, key, values)
			}

		}
	}
	return s
}

func extractOrderFromQuery(values []string, s *search.Search) *search.Search {
// GetQuery retrieves the specified options from a query.
func GetQuery(vals url.Values) (result *query.Query) {
	if len(values) > 0 {
		descending := false
		sortkey := values[0]
		if strings.HasPrefix(sortkey, "-") {
	if exprs, found := vals[api.QueryKeyQuery]; found {
		result = query.Parse(strings.Join(exprs, " "))
			descending = true
			sortkey = sortkey[1:]
		}
	}
		if meta.KeyIsValid(sortkey) || sortkey == search.RandomOrder {
			s = s.AddOrder(sortkey, descending)
		}
	}
	return s
}

	if seeds, found := vals[api.QueryKeySeed]; found {
func extractOffsetFromQuery(values []string, s *search.Search) *search.Search {
	if len(values) > 0 {
		if offset, err := strconv.Atoi(values[0]); err == nil {
			s = s.SetOffset(offset)
		}
	}
	return s
}

		for _, seed := range seeds {
func extractLimitFromQuery(values []string, s *search.Search) *search.Search {
	if len(values) > 0 {
		if limit, err := strconv.Atoi(values[0]); err == nil {
			if si, err := strconv.ParseInt(seed, 10, 31); err == nil {
			s = s.SetLimit(limit)
		}
	}
	return s
}

				result = result.SetSeed(int(si))
func getQueryKeys(forSearch bool) (string, string, string, string, string, string) {
	if forSearch {
				break
		return "sort", "order", "offset", "limit", "negate", "s"
	}
			}
	return "_sort", "_order", "_offset", "_limit", "_negate", "_s"
}

		}
	}
func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search {
	for _, val := range values {
		s = s.AddExpr(key, val)
	}
	return s
	return result
}

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
32
33
34
35
36
37
38
39

40
41

42
43
44
45
46



47
48
49




50
51
52



53
54
55



56








57
58



59
60

61
62
63

64
65







66
67
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18

19
20
21
22

23
24
25
26


27
28





29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

54
55

56
57
58
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

-
+

-
+




+
+
+


-





-

+

+
-
+



-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+








-
+

-
+



-
-
+
+
+

-
-
+
+
+
+

-
-
+
+
+

-
-
+
+
+

+
+
+
+
+
+
+
+
-
-
+
+
+

-
+


-
+


+
+
+
+
+
+
+


//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package adapter provides handlers for web requests.
package adapter

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/place"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// ReportUsecaseError returns an appropriate HTTP status code for errors in use cases.
func ReportUsecaseError(w http.ResponseWriter, err error) {
// WriteData emits the given data to the response writer.
func WriteData(w http.ResponseWriter, data []byte, contentType string) error {
	code, text := CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		log.Printf("%v: %v", text, err)
	}
	http.Error(w, text, code)
	if len(data) == 0 {
		w.WriteHeader(http.StatusNoContent)
		return nil
	}
	PrepareHeader(w, contentType)
	w.WriteHeader(http.StatusOK)
	_, err := w.Write(data)
	return err
}

// PrepareHeader sets the HTTP header to defined values.
func PrepareHeader(w http.ResponseWriter, contentType string) http.Header {
	h := w.Header()
	if contentType != "" {
		h.Set(api.HeaderContentType, contentType)
	}
	return h
}

// 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 == place.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.(*place.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.(*place.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 ezin usecase.ErrZidInUse
	if errors.As(err, &ezin) {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", ezin.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, place.ErrStopped) {
	if errors.Is(err, box.ErrStopped) {
		return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err)
	}
	if errors.Is(err, place.ErrConflict) {
	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()
}

Added 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
49
50
51
52
53
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

// Values for queryKeyAction
const (
	valueActionChild   = "child"
	valueActionCopy    = "copy"
	valueActionFolge   = "folge"
	valueActionNew     = "new"
	valueActionVersion = "version"
)

// Enumeration for queryKeyAction
type createAction uint8

const (
	actionChild createAction = iota
	actionCopy
	actionFolge
	actionNew
	actionVersion
)

var createActionMap = map[string]createAction{
	valueActionChild:   actionChild,
	valueActionCopy:    actionCopy,
	valueActionFolge:   actionFolge,
	valueActionNew:     actionNew,
	valueActionVersion: actionVersion,
}

func getCreateAction(s string) createAction {
	if action, found := createActionMap[s]; found {
		return action
	}
	return actionCopy
}

Changes to web/adapter/webui/create_zettel.go.

1
2

3
4

5
6
7
8



9
10
11
12
13
14

15
16
17

18
19
20
21
22
23
24
25
26
27
28










29
30

31
32

33
34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54

55
56
57
58
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








































1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18

19
20
21










22
23
24
25
26
27
28
29
30
31


32


33












34










35







36
37
38
39
40
41
42


43


44


45


46
47

48
49
50


51
52

53
54
55


56


57
58
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

-
+

-
+




+
+
+


-



+

-

+

-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+


-
-
+
-
-
+
-
-
+
-
-
+

-
+


-
-
+

-
+


-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-

+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+

-
+



+

-
-
-
+
+
+
+
+

-
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+


-
+


-
-
-
+
+
+


-
-
+
+
+
+








+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
)

	"zettelstore.de/z/zettel/id"
// MakeGetCopyZettelHandler creates a new HTTP handler to display the
// HTML edit view of a copied zettel.
	"zettelstore.de/z/zettel/meta"
func (wui *WebUI) MakeGetCopyZettelHandler(getZettel usecase.GetZettel, copyZettel usecase.CopyZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Copy")
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.renderZettelForm(w, r, copyZettel.Run(origZettel), "Copy Zettel", "Copy Zettel")
	}
}

)
// MakeGetFolgeZettelHandler creates a new HTTP handler to display the
// HTML edit view of a follow-up zettel.
func (wui *WebUI) MakeGetFolgeZettelHandler(getZettel usecase.GetZettel, folgeZettel usecase.FolgeZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Folge")
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		wui.renderZettelForm(w, r, folgeZettel.Run(origZettel), "Folge Zettel", "Folgezettel")
	}
}

// MakeGetNewZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeGetNewZettelHandler(getZettel usecase.GetZettel, newZettel usecase.NewZettel) http.HandlerFunc {
// 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) {
		ctx := r.Context()
		origZettel, err := getOrigZettel(ctx, w, r, getZettel, "New")
		if err != nil {
		q := r.URL.Query()
			wui.reportError(ctx, w, err)
			return
		op := getCreateAction(q.Get(queryKeyAction))
		}
		m := origZettel.Meta
		path := r.URL.Path[1:]
		title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk)
		textTitle, err := adapter.FormatInlines(title, "text", nil)
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, err)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}
		env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)}
		htmlTitle, err := adapter.FormatInlines(title, "html", &env)
		origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid})
			return
		}
		wui.renderZettelForm(w, r, newZettel.Run(origZettel), textTitle, htmlTitle)
	}

}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {
		case actionChild:
			wui.renderZettelForm(ctx, w, createZettel.PrepareChild(origZettel), "Child Zettel", "", roleData, syntaxData)
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle))
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		}
func getOrigZettel(
	ctx context.Context,
	w http.ResponseWriter,
	r *http.Request,
	getZettel usecase.GetZettel,
	op string,
) (domain.Zettel, error) {
	if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" {
		return domain.Zettel{}, adapter.NewErrBadRequest(
			fmt.Sprintf("%v zettel not possible in format %q", op, format))
	}
}
	zid, err := id.Parse(r.URL.Path[1:])
	if err != nil {
		return domain.Zettel{}, place.ErrNotFound
	}
	origZettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid)
	if err != nil {
		return domain.Zettel{}, place.ErrNotFound

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 origZettel, nil
	return nil
}

func (wui *WebUI) renderZettelForm(
	ctx context.Context,
	w http.ResponseWriter,
	r *http.Request,
	zettel domain.Zettel,
	title, heading string,
	ztl zettel.Zettel,
	title string,
	formActionURL string,
	roleData []string,
	syntaxData []string,
) {
	ctx := r.Context()
	user := wui.getUser(ctx)
	m := zettel.Meta
	var base baseData
	user := server.GetUser(ctx)
	m := ztl.Meta

	wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, user, &base)
	wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
		Heading:       heading,
		MetaTitle:     m.GetDefault(meta.KeyTitle, ""),
		MetaTags:      m.GetDefault(meta.KeyTags, ""),
		MetaRole:      config.GetRole(m, wui.rtConfig),
		MetaSyntax:    config.GetSyntax(m, wui.rtConfig),
	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.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
	rb.bindString("heading", sx.String(title))
	rb.bindString("form-action-url", sx.String(formActionURL))
	rb.bindString("role-data", makeStringList(roleData))
	rb.bindString("syntax-data", makeStringList(syntaxData))
		MetaPairsRest: m.PairsRest(false),
		IsTextContent: !zettel.Content.IsBinary(),
		Content:       zettel.Content.AsString(),
	rb.bindString("meta", sx.String(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.String(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 {
func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zettel, hasContent, err := parseZettelForm(r, id.Invalid)
		if err != nil {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read form data"))
		reEdit, zettel, err := parseZettelForm(r, id.Invalid)
		if err == errMissingContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
			return
		}
		if !hasContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
		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(newZid.ZettelID()))
		} else {
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid))
	}
}
			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, evaluate *usecase.Evaluate,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {

	return func(w http.ResponseWriter, r *http.Request) {
		q := adapter.GetQuery(r.URL.Query())
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq, 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 := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())}
		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16

17
18




19
20
21
22
23
24

25
26
27
28
29
30
31

32
33
34


35
36
37
38

39
40
41







42
43
44
45
46
47
48







49
50
51
52
53
54
55
56
57
58
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) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"net/http"

	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
				fmt.Sprintf("Delete zettel not possible in format %q", format)))
			return
		}

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		zettel, err := getZettel.Run(ctx, zid)
		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := wui.getUser(ctx)
		m := zettel.Meta
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), user, &base)
		wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct {
			Zid       string
			MetaPairs []meta.Pair
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "delete",
			wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.String(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???")))
			rb.bindString("incoming", nil)
		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		}
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)

		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair {
	zidMap := make(strfun.Set)
	addListValues(zidMap, m, api.KeyBackward)
	for _, kd := range meta.GetSortedKeyDescriptions() {
		inverseKey := kd.Inverse
		if inverseKey == "" {
			continue
		}
		ikd := meta.GetDescription(inverseKey)
		switch ikd.Type {
		case meta.TypeID:
		}{
			Zid:       zid.String(),
			MetaPairs: m.Pairs(true),
		})
	}
}

			if val, ok := m.Get(inverseKey); ok {
				zidMap.Set(val)
			}
		case meta.TypeIDSet:
			addListValues(zidMap, m, inverseKey)
		}
	}
	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.HandlerFunc {
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
	return 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, place.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		if err := deleteZettel.Run(r.Context(), zid); err != nil {
		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('/'))
		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



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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"net/http"

	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"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) http.HandlerFunc {
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return 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, place.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid)
		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Edit zettel %q not possible in format %q", zid, format)))
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		user := wui.getUser(ctx)
		m := zettel.Meta
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData)
		wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
			Heading:       base.Title,
			MetaTitle:     m.GetDefault(meta.KeyTitle, ""),
			MetaRole:      m.GetDefault(meta.KeyRole, ""),
			MetaTags:      m.GetDefault(meta.KeyTags, ""),
			MetaSyntax:    m.GetDefault(meta.KeySyntax, ""),
			MetaPairsRest: m.PairsRest(false),
			IsTextContent: !zettel.Content.IsBinary(),
			Content:       zettel.Content.AsString(),
		})
	}
}

// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc {
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return 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, place.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zettel, hasContent, err := parseZettelForm(r, zid)
		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("Unable to read zettel form"))
			return
		}

		if err := updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
				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(zid.ZettelID()))
		} else {
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid))
	}
}
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID()))
		}
	}
}

Added 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
44
45
46
47
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
)

func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			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.Error().Err(err).Msg("Unable to read favicon data")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}

		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



















































1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26




27
28
29
30
31
32
33
34







35
36



37
38
39
40
41


42
43
44
45

46
47
48
49
50
51

52
53
54
55
56
57

58
59
60


61
62
63
64
65
66
67
68

69
70
71

72
73




74
75
76
77
78


79
80



81
82
83
84
85

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

-
+

-
+




+
+
+


-



+
+
+

+

+

+
+
-
-
-
-
+
+
+
+
+
+


-
-
-
-
-
-
-
+
+
-
-
-
+
+
+
+

-
-
+
+
+

-
+

+



-
+
+




-
+


-
-
+
+
+
+
+



-
+


-
+

-
-
-
-
+
+
+
+
+
-
-
+

-
-
-
+
+
+
+
+
-











+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"errors"
	"io"
	"net/http"
	"regexp"
	"strings"
	"unicode"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"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"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

type formZettelData struct {
	Heading       string
	MetaTitle     string
	MetaRole      string
	MetaTags      string
	MetaSyntax    string
	MetaPairsRest []meta.Pair
var (
	bsCRLF = []byte{'\r', '\n'}
	IsTextContent bool
	Content       string
}
	bsLF   = []byte{'\n'}
)

var errMissingContent = errors.New("missing zettel content")

func parseZettelForm(r *http.Request, zid id.Zid) (domain.Zettel, bool, error) {
	err := r.ParseForm()
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 domain.Zettel{}, false, 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(postMeta))
		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(meta.KeyTitle, postTitle)
		m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
	}
	if postTags, ok := trimmedFormValue(r, "tags"); ok {
		if tags := strings.Fields(postTags); len(tags) > 0 {
			m.SetList(meta.KeyTags, tags)
		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(meta.KeyRole, postRole)
		m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole))
	}
	if postSyntax, ok := trimmedFormValue(r, "syntax"); ok {
		m.Set(meta.KeySyntax, postSyntax)
		m.SetWord(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
	}
	if values, ok := r.PostForm["content"]; ok && len(values) > 0 {
		return domain.Zettel{
			Meta: m,
			Content: domain.NewContent(

	if data := textContent(r); data != nil {
		return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil
	}
	if data, m2 := uploadedContent(r, m); data != nil {
				strings.ReplaceAll(strings.TrimSpace(values[0]), "\r\n", "\n")),
		}, true, nil
		return doSave, zettel.Zettel{Meta: m2, Content: zettel.NewContent(data)}, nil
	}
	return domain.Zettel{
		Meta:    m,
		Content: domain.NewContent(""),

	if allowEmptyContent(m) {
		return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, nil
	}
	return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, errMissingContent
	}, false, nil
}

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
		}
	}
	return "", false
}

func textContent(r *http.Request) []byte {
	if values, found := r.PostForm["content"]; found && len(values) > 0 {
		result := bytes.ReplaceAll([]byte(values[0]), bsCRLF, bsLF)
		if bytes.IndexFunc(result, func(ch rune) bool { return !unicode.IsSpace(ch) }) >= 0 {
			return result
		}
	}
	return nil
}

func uploadedContent(r *http.Request, m *meta.Meta) ([]byte, *meta.Meta) {
	file, fh, err := r.FormFile("file")
	if file != nil {
		defer file.Close()
		if err == nil {
			data, err2 := io.ReadAll(file)
			if err2 != nil {
				return nil, m
			}
			if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 {
				ct := cts[0]
				if fileSyntax := content.SyntaxFromMIME(ct, data); fileSyntax != "" {
					m = m.Clone()
					m.Set(api.KeySyntax, fileSyntax)
				}
			}
			return data, m
		}
	}
	return nil, m
}

func allowEmptyContent(m *meta.Meta) bool {
	if syntax, found := m.Get(api.KeySyntax); found {
		if syntax == api.ValueSyntaxNone {
			return true
		}
		if pinfo := parser.Get(syntax); pinfo != nil {
			return pinfo.IsTextFormat
		}
	}
	return true
}

var reEmptyLines = regexp.MustCompile(`(\n|\r)+\s*(\n|\r)+`)

func removeEmptyLines(s []byte) []byte {
	b := bytes.TrimSpace(s)
	return reEmptyLines.ReplaceAllLiteral(b, []byte{'\n'})
}

Added web/adapter/webui/forms_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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) {
	t.Parallel()
	testcases := []struct {
		in  string
		exp string
	}{
		{"", ""},
		{"a", "a"},
		{"\na", "a"},
		{"a\n", "a"},
		{"a\nb", "a\nb"},
		{"a\n\nb", "a\nb"},
		{"a\n \nb", "a\nb"},
	}
	for i, tc := range testcases {
		got := string(removeEmptyLines([]byte(tc.in)))
		if got != tc.exp {
			t.Errorf("%d/%q: expected=%q, got=%q", i, tc.in, tc.exp, got)
		}
	}
}

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












1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16

17
18
19
20
21
22
23
24







25
26
27
28
29
30
31
32

33


34





35





36




37

38
39
40
41
42
43
44
45
46
47


48
49
50
51
52
53
54
55
56
57
58

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

-
+

-
+




+
+
+


-



-
+

+


+
+

-
-
-
-
-
-
-
+
+
+
+
+
+
+

-
+
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-

-
+
+
+
+
+
+
+



-
-
+
+
+
+
+
+
+
+
+
+
+
-



+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+

-
+



+
+
-
+




-
+
-
-
+

-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-

-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
+
-
-
-
+
+
-
-
-
-
+
+


-
+
+
+
+
+



-
+


-
+

-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
+



+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"context"
	"net/http"
	"sort"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/encfun"
	"zettelstore.de/z/place"
	"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/adapter"
	"zettelstore.de/z/web/server"
)

	"zettelstore.de/z/zettel/id"
type metaDataInfo struct {
	Key   string
	Value string
}

)
type matrixElement struct {
	Text   string
	HasURL bool
	URL    string
}

type matrixLine struct {
	Elements []matrixElement
}

// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc {
func (wui *WebUI) MakeGetInfoHandler(
	ucParseZettel usecase.ParseZettel,
	ucEvaluate *usecase.Evaluate,
	ucGetZettel usecase.GetZettel,
	ucGetAllMeta usecase.GetAllZettel,
	ucQuery *usecase.Query,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		if format := adapter.GetFormat(r, q, "html"); format != "html" {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(

		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
				fmt.Sprintf("Zettel info not available in format %q", format)))
			return
		}

		enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
		getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
		evalMeta := func(val string) ast.InlineSlice {
			return ucEvaluate.RunMetadata(ctx, val)
		}
		pairs := zn.Meta.ComputedPairs()
		metadata := sx.Nil()
		for i := len(pairs) - 1; i >= 0; i-- {
			key := pairs[i].Key
			sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
			metadata = metadata.Cons(sx.Cons(sx.String(key), sxval))
		}
		zid, err := id.Parse(r.URL.Path[1:])

		summary := collect.References(zn)
		locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = title
		}
		unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase))
		if err != nil {
			wui.reportError(ctx, w, place.ErrNotFound)
			wui.reportError(ctx, w, err)
			return
		}

		entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)
		bns := ucEvaluate.RunBlockNode(ctx, entries)
		zn, err := parseZettel.Run(ctx, zid, q.Get("syntax"))
		unlinkedContent, _, err := enc.BlocksSxn(&bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		encTexts := encodingTexts()
		summary := collect.References(zn)
		locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Images...))
		shadowLinks := getShadowLinks(ctx, zid, ucGetAllMeta)

		lang := config.GetLang(zn.InhMeta, wui.rtConfig)
		env := encoder.Environment{Lang: lang}
		pairs := zn.Meta.Pairs(true)
		metaData := make([]metaDataInfo, len(pairs))
		getTitle := makeGetTitle(ctx, getMeta, &env)
		for i, p := range pairs {
			var html strings.Builder
			wui.writeHTMLMetaValue(&html, zn.Meta, p.Key, getTitle, &env)
			metaData[i] = metaDataInfo{p.Key, html.String()}
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
		rb.bindString("metadata", metadata)
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.String(phrase))
		rb.bindString("query-key-phrase", sx.String(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env)
		} else {
			err = rb.err
		}
		endnotes, err := formatBlocks(nil, "html", &env)
		if err != nil {
			endnotes = ""
		}

			wui.reportError(ctx, w, err)
		textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle)
		user := wui.getUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, lang, textTitle, user, &base)
		wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct {
			Zid          string
			WebURL       string
			ContextURL   string
			CanWrite     bool
			EditURL      string
			CanFolge     bool
			FolgeURL     string
			CanCopy      bool
			CopyURL      string
			CanRename    bool
			RenameURL    string
			CanDelete    bool
			DeleteURL    string
			MetaData     []metaDataInfo
			HasLinks     bool
			HasLocLinks  bool
			LocLinks     []localLink
			HasExtLinks  bool
			ExtLinks     []string
			ExtNewWindow string
			Matrix       []matrixLine
			Endnotes     string
		}{
			Zid:          zid.String(),
			WebURL:       wui.NewURLBuilder('h').SetZid(zid).String(),
			ContextURL:   wui.NewURLBuilder('j').SetZid(zid).String(),
			CanWrite:     wui.canWrite(ctx, user, zn.Meta, zn.Content),
			EditURL:      wui.NewURLBuilder('e').SetZid(zid).String(),
			CanFolge:     base.CanCreate,
			FolgeURL:     wui.NewURLBuilder('f').SetZid(zid).String(),
			CanCopy:      base.CanCreate && !zn.Content.IsBinary(),
			CopyURL:      wui.NewURLBuilder('c').SetZid(zid).String(),
			CanRename:    wui.canRename(ctx, user, zn.Meta),
			RenameURL:    wui.NewURLBuilder('b').SetZid(zid).String(),
			CanDelete:    wui.canDelete(ctx, user, zn.Meta),
			DeleteURL:    wui.NewURLBuilder('d').SetZid(zid).String(),
			MetaData:     metaData,
			HasLinks:     len(extLinks)+len(locLinks) > 0,
			HasLocLinks:  len(locLinks) > 0,
			LocLinks:     locLinks,
			HasExtLinks:  len(extLinks) > 0,
			ExtLinks:     extLinks,
			ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0),
			Matrix:       wui.infoAPIMatrix(zid),
			Endnotes:     endnotes,
		})
	}
}

		}
	}
}
type localLink struct {
	Valid bool
	Zid   string
}


func splitLocExtLinks(links []*ast.Reference) (locLinks []localLink, extLinks []string) {
	if len(links) == 0 {
func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) {
	for i := len(links) - 1; i >= 0; i-- {
		return nil, nil
	}
	for _, ref := range links {
		if ref.State == ast.RefStateSelf {
		ref := links[i]
		if ref.State == ast.RefStateSelf || ref.IsZettel() {
			continue
		}
		if ref.IsZettel() {
		if ref.State == ast.RefStateQuery {
			queries = queries.Cons(
				sx.Cons(
					sx.String(ref.Value),
					sx.String(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))
			continue
		}
		if ref.IsExternal() {
			extLinks = append(extLinks, ref.String())
			extLinks = extLinks.Cons(sx.String(ref.String()))
			continue
		}
		locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()})
		locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.String(ref.String())))
	}
	return locLinks, extLinks
	return locLinks, queries, extLinks
}

func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
	var sb strings.Builder
	sb.Write(zid.Bytes())
	sb.WriteByte(' ')
	sb.WriteString(api.UnlinkedDirective)
	for _, word := range strfun.MakeWords(phrase) {
		sb.WriteByte(' ')
		sb.WriteString(api.PhraseDirective)
		sb.WriteByte(' ')
		sb.WriteString(word)
	}
	sb.WriteByte(' ')
	sb.WriteString(api.OrderDirective)
	sb.WriteByte(' ')
	sb.WriteString(api.KeyID)
	return query.Parse(sb.String())
}

func encodingTexts() []string {
	encodings := encoder.GetEncodings()
	encTexts := make([]string, 0, len(encodings))
	for _, f := range encodings {
		encTexts = append(encTexts, f.String())
	}
	sort.Strings(encTexts)
	return encTexts
}

var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent}

func (wui *WebUI) infoAPIMatrix(zid id.Zid) []matrixLine {
func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair {
	formats := encoder.GetFormats()
	defFormat := encoder.GetDefaultFormat()
	parts := []string{"zettel", "meta", "content"}
	matrix := make([]matrixLine, 0, len(parts))
	u := wui.NewURLBuilder('z').SetZid(zid)
	for _, part := range parts {
		row := make([]matrixElement, 0, len(formats)+1)
	matrix := sx.Nil()
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
	for ip := len(apiParts) - 1; ip >= 0; ip-- {
		part := apiParts[ip]
		row := sx.Nil()
		row = append(row, matrixElement{part, false, ""})
		for _, format := range formats {
			u.AppendQuery("_part", part)
			if format != defFormat {
				u.AppendQuery("_format", format)
			}
			row = append(row, matrixElement{format, true, u.String()})
		for je := len(encTexts) - 1; je >= 0; je-- {
			enc := encTexts[je]
			if parseOnly {
				u.AppendKVQuery(api.QueryKeyParseOnly, "")
			}
			u.AppendKVQuery(api.QueryKeyPart, part)
			u.AppendKVQuery(api.QueryKeyEncoding, enc)
			row = row.Cons(sx.Cons(sx.String(enc), sx.String(u.String())))
			u.ClearQuery()
		}
		matrix = matrix.Cons(sx.Cons(sx.String(part), row))
	}
	return matrix
}

func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair {
	matrix := wui.infoAPIMatrix(zid, true, encTexts)
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())

	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.String("plain"), sx.String(u.String())))
		u.ClearQuery()
		if i < 2 {
			u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
			u.AppendKVQuery(api.QueryKeyPart, part)
			last.AppendBang(sx.Cons(sx.String("data"), sx.String(u.String())))
			u.ClearQuery()
		}
		matrix = append(matrix, matrixLine{row})
		i++
	}
	return matrix
}

func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair {
	result := sx.Nil()
	if zl, err := getAllZettel.Run(ctx, zid); err == nil {
		for i := len(zl) - 1; i >= 1; i-- {
			if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
				result = result.Cons(sx.String(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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16

17
18
19
20
21
22

23
24
25



26
27
28
29



30

31
32
33

34
35
36
37

38
39

40
41
42
43


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58














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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"net/http"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/encoder/encfun"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/meta"
	"zettelstore.de/z/web/adapter"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc {
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc {
	return 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, place.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		syntax := r.URL.Query().Get("syntax")
		zn, err := parseZettel.Run(ctx, zid, syntax)
		q := r.URL.Query()
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
		metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		content, endnotes, err := enc.BlocksSxn(&zn.Ast)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		lang := config.GetLang(zn.InhMeta, wui.rtConfig)
		envHTML := encoder.Environment{
			LinkAdapter:    adapter.MakeLinkAdapter(ctx, wui, 'h', getMeta, "", ""),
			ImageAdapter:   adapter.MakeImageAdapter(ctx, wui, getMeta),
			CiteAdapter:    nil,
			Lang:           lang,
			Xhtml:          false,
			MarkerExternal: wui.rtConfig.GetMarkerExternal(),
			NewWindow:      true,
			IgnoreMeta:     map[string]bool{meta.KeyTitle: true, meta.KeyLang: true},
		}
		metaHeader, err := formatMeta(zn.InhMeta, "html", &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
		user := server.GetUser(ctx)
			return
		}
		htmlTitle, err := adapter.FormatInlines(
			encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), "html", &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)
			return
		}
		htmlContent, err := formatBlocks(zn.Ast, "html", &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle)

		user := wui.getUser(ctx)
		roleText := zn.Meta.GetDefault(meta.KeyRole, "*")
		tags := wui.buildTagInfos(zn.Meta)
		getTitle := makeGetTitle(ctx, getMeta, &encoder.Environment{Lang: lang})
		extURL, hasExtURL := zn.Meta.Get(meta.KeyURL)
		backLinks := wui.formatBackLinks(zn.InhMeta, getTitle)
		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user)
		rb.bindSymbol(symMetaHeader, metaObj)
		rb.bindString("heading", sx.String(title))
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			rb.bindString("role-url", sx.String(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
		}
		if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" {
		var base baseData
		wui.makeBaseData(ctx, lang, textTitle, user, &base)
		base.MetaHeader = metaHeader
			rb.bindString("folge-role-url", sx.String(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+folgeRole).String()))
		wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
			HTMLTitle     string
			CanWrite      bool
			EditURL       string
			Zid           string
			InfoURL       string
			RoleText      string
			RoleURL       string
			HasTags       bool
			Tags          []simpleLink
			CanCopy       bool
			CopyURL       string
			CanFolge      bool
		}
		rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
		rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
		rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
		rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle))
		rb.bindString("urls", metaURLAssoc(zn.InhMeta))
		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, api.KeySubordinates, config.KeyShowSubordinateLinks, 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 != "" {
			FolgeURL      string
			FolgeRefs     string
			PrecursorRefs string
			HasExtURL     bool
			ExtURL        string
			ExtNewWindow  string
			Content       string
			for _, part := range []string{"meta", "actions", "heading"} {
				rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part)
			}
		}
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
			HasBackLinks  bool
			BackLinks     []simpleLink
		}{
			HTMLTitle:     htmlTitle,
			CanWrite:      wui.canWrite(ctx, user, zn.Meta, zn.Content),
			EditURL:       wui.NewURLBuilder('e').SetZid(zid).String(),
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, env)
		} else {
			Zid:           zid.String(),
			InfoURL:       wui.NewURLBuilder('i').SetZid(zid).String(),
			RoleText:      roleText,
			err = rb.err
			RoleURL:       wui.NewURLBuilder('h').AppendQuery("role", roleText).String(),
			HasTags:       len(tags) > 0,
			Tags:          tags,
			CanCopy:       base.CanCreate && !zn.Content.IsBinary(),
			CopyURL:       wui.NewURLBuilder('c').SetZid(zid).String(),
			CanFolge:      base.CanCreate,
			FolgeURL:      wui.NewURLBuilder('f').SetZid(zid).String(),
			FolgeRefs:     wui.formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle),
			PrecursorRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle),
			ExtURL:        extURL,
		}
		if err != nil {
			HasExtURL:     hasExtURL,
			ExtNewWindow:  htmlAttrNewWindow(envHTML.NewWindow && hasExtURL),
			Content:       htmlContent,
			HasBackLinks:  len(backLinks) > 0,
			wui.reportError(ctx, w, err)
		}
	}
}
			BackLinks:     backLinks,
		})
	}
}

func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	if values, ok := m.GetList(key); ok {
		return wui.transformIdentifierSet(values, getTextTitle)
	}

func formatBlocks(bs ast.BlockSlice, format string, env *encoder.Environment) (string, error) {
	enc := encoder.Create(format, env)
	if enc == nil {
		return "", adapter.ErrNoSuchFormat
	}
	return nil
}

func metaURLAssoc(m *meta.Meta) *sx.Pair {
	var content strings.Builder
	_, err := enc.WriteBlocks(&content, bs)
	if err != nil {
	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.String(capitalizeMetaKey(key)), sx.String(val)))
		return "", err
	}
			}
	return content.String(), nil
}

		}
	}
func formatMeta(m *meta.Meta, format string, env *encoder.Environment) (string, error) {
	enc := encoder.Create(format, env)
	if enc == nil {
		return "", adapter.ErrNoSuchFormat
	}
	return result.List()
}

	var content strings.Builder
	_, err := enc.WriteMeta(&content, m)
	if err != nil {
		return "", err
	}
	return content.String(), nil
}


func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) {
	varLinks := varPrefix + "-links"
	var symOpen *sx.Symbol
	switch wui.rtConfig.Get(ctx, m, configKey) {
	case "false":
		rb.bindString(varLinks, sx.Nil())
		return
	case "close":
	default:
		symOpen = shtml.SymAttrOpen
	}
	lstLinks := wui.zettelLinksSxn(m, key, getTextTitle)
func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink {
	var tagInfos []simpleLink
	rb.bindString(varLinks, lstLinks)
	if tags, ok := m.GetList(meta.KeyTags); ok {
		ub := wui.NewURLBuilder('h')
		tagInfos = make([]simpleLink, len(tags))
	if sx.IsNil(lstLinks) {
		for i, tag := range tags {
			tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery("tags", tag).String()}
			ub.ClearQuery()
		}
	}
		return
	}
	rb.bindString(varPrefix+"-open", symOpen)
}
	return tagInfos
}


func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
func (wui *WebUI) formatMetaKey(m *meta.Meta, key string, getTitle getTitleFunc) string {
	if _, ok := m.Get(key); ok {
	values, ok := m.GetList(key)
		var buf bytes.Buffer
		wui.writeHTMLMetaValue(&buf, m, key, getTitle, nil)
		return buf.String()
	if !ok || len(values) == 0 {
		return nil
	}
	return ""
	return wui.zidLinksSxn(values, getTextTitle)
}

func (wui *WebUI) formatBackLinks(m *meta.Meta, getTitle getTitleFunc) []simpleLink {
func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) {
	values, ok := m.GetList(meta.KeyBack)
	if !ok || len(values) == 0 {
	for i := len(values) - 1; i >= 0; i-- {
		return nil
	}
	result := make([]simpleLink, 0, len(values))
	for _, val := range values {
		val := values[i]
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTitle(zid, "text"); found > 0 {
			url := wui.NewURLBuilder('h').SetZid(zid).String()
		if title, found := getTextTitle(zid); found > 0 {
			url := sx.String(wui.NewURLBuilder('h').SetZid(zid.ZettelID()).String())
			if title == "" {
				result = append(result, simpleLink{Text: val, URL: url})
				lst = lst.Cons(sx.Cons(sx.String(val), url))
			} else {
				result = append(result, simpleLink{Text: title, URL: url})
				lst = lst.Cons(sx.Cons(sx.String(title), url))
			}
		}
	}
	return result
	return lst
}

Added 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
33
34
35
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package 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) {
		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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22



23
24
25
26
27
28
29


30
31
32
33
34
35
36


37
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"errors"
	"net/http"

	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

type getRootStore 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) {
		ctx := r.Context()
		if r.URL.Path != "/" {
			wui.reportError(ctx, w, place.ErrNotFound)
		if p := r.URL.Path; p != "/" {
			wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p})
			return
		}
		homeZid := wui.rtConfig.GetHomeZettel()
		homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel))
		apiHomeZid := homeZid.ZettelID()
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetMeta(ctx, homeZid); err == nil {
				redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
			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 {
			redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
			return
		}
		if errors.Is(err, &place.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil {
			redirectFound(w, r, wui.NewURLBuilder('a'))
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('i'))
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('h'))
		wui.redirectFound(w, r, wui.NewURLBuilder('h'))
	}
}

Added 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
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-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European 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 (
	"net/url"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/client.fossil/sz"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	tx    *szenc.Transformer
	th    *shtml.Evaluator
	lang  string
	symAt *sx.Symbol
}

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
		}

		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(string(href), "#")
		u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(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 {
		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
		}
		u := builder.NewURLBuilder('/').SetRawLocal(string(href))
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(u.String())))
		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(string(href))
		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.String(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrClass, sx.String("external"))).
			Cons(sx.Cons(shtml.SymAttrTarget, sx.String("_blank"))).
			Cons(sx.Cons(shtml.SymAttrRel, sx.String("noopener noreferrer")))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).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)
		if !zid.IsValid() {
			return obj
		}
		u := builder.NewURLBuilder('z').SetZid(zid)
		imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.String(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) 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)
	metaMap := make(map[string]*sx.Pair, m.Length())
	if tags, ok := m.Get(api.KeyTags); ok {
		metaMap[api.KeyTags] = g.transformMetaTags(tags)
		ignore.Set(api.KeyTags)
	}

	for elem := hm; elem != nil; elem = elem.Tail() {
		mlst, isPair := sx.GetPair(elem.Car())
		if !isPair {
			continue
		}
		att, isPair := sx.GetPair(mlst.Tail().Car())
		if !isPair {
			continue
		}
		if !att.Car().IsEqual(g.symAt) {
			continue
		}
		a := make(attrs.Attributes, 32)
		for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() {
			if p, ok := sx.GetPair(aelem.Car()); ok {
				key := p.Car()
				val := p.Cdr()
				if tail, isTail := sx.GetPair(val); isTail {
					val = tail.Car()
				}
				a = a.Set(sz.GoValue(key), sz.GoValue(val))
			}
		}
		name, found := a.Get("name")
		if !found || ignore.Has(name) {
			continue
		}

		newName, found := mapMetaKey[name]
		if !found {
			continue
		}
		a = a.Set("name", newName)
		metaMap[newName] = g.th.EvaluateMeta(a)
	}
	result := sx.Nil()
	keys := maps.Keys(metaMap)
	for i := len(keys) - 1; i >= 0; i-- {
		result = result.Cons(metaMap[keys[i]])
	}
	return result
}

func (g *htmlGenerator) transformMetaTags(tags string) *sx.Pair {
	var sb strings.Builder
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			sb.WriteString(", ")
		}
		sb.WriteString(strings.TrimPrefix(val, "#"))
	}
	metaTags := sb.String()
	if len(metaTags) == 0 {
		return nil
	}
	return g.th.EvaluateMeta(attrs.Attributes{"name": "keywords", "content": metaTags})
}

func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) {
	if bs == nil || len(*bs) == 0 {
		return nil, nil, nil
	}
	sx := g.tx.GetSz(bs)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil, nil, err
	}
	return sh, g.th.Endnotes(&env), 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)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return 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

194
195
196
197
198
199
200
201
202


203
204
205






1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18




19






20
21
22
23
24
25
26
27

28
29
30
31




32
33
34
35
36
37
38


39

40
41

42
43

44
45


46

47

48
49

50
51


52

53


54
55
56
57
58
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"time"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/strfun"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

var space = []byte{' '}

func (wui *WebUI) writeHTMLMetaValue(w io.Writer, m *meta.Meta, key string, getTitle getTitleFunc, env *encoder.Environment) {
	switch kt := m.Type(key); kt {
func (wui *WebUI) writeHTMLMetaValue(
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
) sx.Object {
	switch kt := meta.Type(key); kt {
	case meta.TypeBool:
		wui.writeHTMLBool(w, key, m.GetBool(key))
	case meta.TypeCredential:
		writeCredential(w, m.GetDefault(key, "???c"))
		return sx.String(value)
	case meta.TypeEmpty:
		writeEmpty(w, m.GetDefault(key, "???e"))
		return sx.String(value)
	case meta.TypeID:
		wui.writeIdentifier(w, m.GetDefault(key, "???i"), getTitle)
		return wui.transformIdentifier(value, getTextTitle)
	case meta.TypeIDSet:
		if l, ok := m.GetList(key); ok {
			wui.writeIdentifierSet(w, l, getTitle)
		return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
		}
	case meta.TypeNumber:
		writeNumber(w, m.GetDefault(key, "???n"))
		return wui.transformKeyValueText(key, value, value)
	case meta.TypeString:
		writeString(w, m.GetDefault(key, "???s"))
		return sx.String(value)
	case meta.TypeTagSet:
		if l, ok := m.GetList(key); ok {
			wui.writeTagSet(w, key, l)
		return wui.transformTagSet(key, meta.ListFromValue(value))
		}
	case meta.TypeTimestamp:
		if ts, ok := m.GetTime(key); ok {
			writeTimestamp(w, ts)
		if ts, ok := meta.TimeValue(value); ok {
			return sx.MakeList(
				sx.MakeSymbol("time"),
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(sx.MakeSymbol("datetime"), sx.String(ts.Format("2006-01-02T15:04:05"))),
				),
				sx.MakeList(sxhtml.SymNoEscape, sx.String(ts.Format("2006-01-02&nbsp;15:04:05"))),
			)
		}
		return sx.Nil()
	case meta.TypeURL:
		writeURL(w, m.GetDefault(key, "???u"))
		return wui.url2html(sx.String(value))
	case meta.TypeWord:
		wui.writeWord(w, key, m.GetDefault(key, "???w"))
	case meta.TypeWordSet:
		if l, ok := m.GetList(key); ok {
			wui.writeWordSet(w, key, l)
		return wui.transformKeyValueText(key, value, value)
		}
	case meta.TypeZettelmarkup:
		writeZettelmarkup(w, m.GetDefault(key, "???z"), env)
	case meta.TypeUnknown:
		writeUnknown(w, m.GetDefault(key, "???u"))
		return wui.transformZmkMetadata(value, evalMetadata, gen)
	default:
		strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
	}
}

		return sx.MakeList(shtml.SymSTRONG, sx.String("Unhandled type: "), sx.String(kt.Name))
func (wui *WebUI) writeHTMLBool(w io.Writer, key string, val bool) {
	if val {
		wui.writeLink(w, key, "true", "True")
	} else {
		wui.writeLink(w, key, "false", "False")
	}
}

func writeCredential(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
func writeEmpty(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

	text := sx.String(val)
func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTitle func(id.Zid, string) (string, int)) {
	zid, err := id.Parse(val)
	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
		return text
	}
	title, found := getTitle(zid, "text")
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(zid.ZettelID())
		attrs := sx.Nil()
		if title == "" {
		if title != "" {
			fmt.Fprintf(w, "<a href=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), zid)
		} else {
			fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), title, zid)
			attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.String(title)))
		}
		attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.String(ub.String()))).Cons(sxhtml.SymAttr)
		return sx.Nil().Cons(sx.String(zid.String())).Cons(attrs).Cons(shtml.SymA)
	case found == 0:
		fmt.Fprintf(w, "<s>%v</s>", val)
	case found < 0:
		return sx.MakeList(sx.MakeSymbol("s"), text)
	default: // case found < 0:
		io.WriteString(w, val)
	}
}

		return text
func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTitle func(id.Zid, string) (string, int)) {
	for i, val := range vals {
		if i > 0 {
			w.Write(space)
		}
	}
		wui.writeIdentifier(w, val, getTitle)
	}
}
}

func writeNumber(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func writeString(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}


func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
	if len(vals) == 0 {
		return nil
	}
	const space = sx.String(" ")
	text := make(sx.Vector, 0, 2*len(vals))
	for _, val := range vals {
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
	}
	return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
func writeUnknown(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) {
	for i, tag := range tags {
		if i > 0 {
func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
	if len(tags) == 0 {
		return nil
			w.Write(space)
		}
		wui.writeLink(w, key, tag, tag)
	}
	const space = sx.String(" ")
	text := make(sx.Vector, 0, 2*len(tags)+2)
	for _, tag := range tags {
		text = append(text, space, wui.transformKeyValueText(key, tag, tag))
	}
}

	if len(tags) > 1 {
func writeTimestamp(w io.Writer, ts time.Time) {
	io.WriteString(w, ts.Format("2006-01-02&nbsp;15:04:05"))
}

func writeURL(w io.Writer, val string) {
	u, err := url.Parse(val)
	if err != nil {
		text = append(text, space, wui.transformKeyValuesText(key, tags, "(all)"))
	}
	return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
}

func (wui *WebUI) transformKeyValueText(key, value, text string) *sx.Pair {
	ub := wui.NewURLBuilder('h').AppendQuery(key + api.SearchOperatorHas + value)
		strfun.HTMLEscape(w, val, false)
		return
	}
	return buildHref(ub, text)
}
	fmt.Fprintf(w, "<a href=\"%v\">", u)
	strfun.HTMLEscape(w, val, false)
	io.WriteString(w, "</a>")
}


func (wui *WebUI) transformKeyValuesText(key string, values []string, text string) *sx.Pair {
func (wui *WebUI) writeWord(w io.Writer, key, word string) {
	wui.writeLink(w, key, word, word)
}

	ub := wui.NewURLBuilder('h')
func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) {
	for i, word := range words {
	for _, val := range values {
		if i > 0 {
			w.Write(space)
		}
		ub = ub.AppendQuery(key + api.SearchOperatorHas + val)
	}
		wui.writeWord(w, key, word)
	}
}
func writeZettelmarkup(w io.Writer, val string, env *encoder.Environment) {
	return buildHref(ub, text)
}

func buildHref(ub *api.URLBuilder, text string) *sx.Pair {
	title, err := adapter.FormatInlines(parser.ParseMetadata(val), "html", env)
	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
	}
	return sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.String(ub.String())),
		),
		sx.String(text),
	)
}
	io.WriteString(w, title)
}

type evalMetadataFunc = func(string) ast.InlineSlice

func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
func (wui *WebUI) writeLink(w io.Writer, key, value, text string) {
	return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }
	fmt.Fprintf(w, "<a href=\"%v?%v=%v\">", wui.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value))
	strfun.HTMLEscape(w, text, false)
	io.WriteString(w, "</a>")
}

type getTitleFunc func(id.Zid, string) (string, int)
type getTextTitleFunc func(id.Zid) (string, int)

func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, env *encoder.Environment) getTitleFunc {
	return func(zid id.Zid, format string) (string, int) {
		m, err := getMeta.Run(place.NoEnrichContext(ctx), zid)
func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc {
	return func(zid id.Zid) (string, int) {
		z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			if errors.Is(err, &place.ErrNotAllowed{}) {
			if errors.Is(err, &box.ErrNotAllowed{}) {
				return "", -1
			}
			return "", 0
		}
		astTitle := parser.ParseMetadata(m.GetDefault(meta.KeyTitle, ""))
		title, err := adapter.FormatInlines(astTitle, format, env)
		if err == nil {
			return title, 1
		}
		return parser.NormalizedSpacedText(z.Meta.GetTitle()), 1
	}
		return "", 1
	}
}
}

func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sx.Object {
	is := evalMetadata(value)
	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
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20

21
22
23
24

25







26
27
28
29
30
31


32


33







34


35


36
37



38
39


40
41









42
43
44
45
46
47
48
49
50
51
52
53
54
55





56






57
58


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

-
+

-
+




+
+
+


-




+


-
+



-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
-
-
+
-
-
-
-
-
-
-
+
-
-
+
-
-
+
+
-
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
-
-
-
-
-
-
+
+
-
-
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
-
-
+
-
-
-
+
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+

-
-
-
-
-


-

-
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
-
-
+
-
-
+
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+


-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
-
-
-

-
+
-

+
-
-
-
-
+
+
+
+
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+

-
-
+
+
+
+
+
-

-
-
-
-
+
+
-

-


-
-
+
+
-
-
+
-
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
-
-
-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"io"
	"net/http"
	"net/url"
	"sort"
	"slices"
	"strconv"
	"strings"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
)

	"zettelstore.de/z/encoding/xml"
// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of
// zettel as HTML.
	"zettelstore.de/z/evaluator"
func (wui *WebUI) MakeListHTMLMetaHandler(
	listMeta usecase.ListMeta,
	listRole usecase.ListRole,
	listTags usecase.ListTags,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
	"zettelstore.de/z/query"
		switch query.Get("_l") {
		case "r":
	"zettelstore.de/z/usecase"
			wui.renderRolesList(w, r, listRole)
		case "t":
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
			wui.renderTagsList(w, r, listTags)
		default:
			wui.renderZettelList(w, r, listMeta)
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
		}
	}
)

}

func (wui *WebUI) renderZettelList(w http.ResponseWriter, r *http.Request, listMeta usecase.ListMeta) {
	query := r.URL.Query()
	s := adapter.GetSearch(query, false)
	ctx := r.Context()
	title := wui.listTitleSearch("Filter", s)
	wui.renderMetaList(
		ctx, w, title, s,
// 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.HandlerFunc {
	return 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()
		metaSeq, err := queryMeta.Run(ctx, q)
		if err != nil {
			wui.reportError(ctx, w, err)
		func(s *search.Search) ([]*meta.Meta, error) {
			if !s.HasComputedMetaKey() {
				ctx = place.NoEnrichContext(ctx)
			}
			return listMeta.Run(ctx, s)
			return
		},
		func(offset int) string {
			return wui.newPageURL('h', query, offset, "_offset", "_limit")
		})
}

		}
		actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex)
type roleInfo struct {
	Text string
		if err != nil {
			wui.reportError(ctx, w, err)
			return
	URL  string
}

func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRole) {
	ctx := r.Context()
		}
		if len(actions) > 0 {
			if len(metaSeq) > 0 {
				for _, act := range actions {
	roleList, err := listRole.Run(ctx)
	if err != nil {
		adapter.ReportUsecaseError(w, err)
		return
	}

					if act == api.RedirectAction {
						ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid.ZettelID())
						wui.redirectFound(w, r, ub)
						return
					}
				}
	roleInfos := make([]roleInfo, 0, len(roleList))
	for _, role := range roleList {
		roleInfos = append(
			roleInfos,
			}
			switch actions[0] {
			case api.AtomAction:
				wui.renderAtom(w, q, metaSeq)
				return
			roleInfo{role, wui.NewURLBuilder('h').AppendQuery("role", role).String()})
	}

			case api.RSSAction:
				wui.renderRSS(ctx, w, q, metaSeq)
				return
			}
		}
	user := wui.getUser(ctx)
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
	wui.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct {
		Roles []roleInfo
	}{
		Roles: roleInfos,
	})
}


		var content, endnotes *sx.Pair
type countInfo struct {
	Count string
		numEntries := 0
	URL   string
}

		if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil {
type tagInfo struct {
	Name  string
	URL   string
	count int
	Count string
	Size  string
}

			enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, nil, api.KeyLang))
var fontSizes = [...]int{75, 83, 100, 117, 150, 200}

			content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
func (wui *WebUI) renderTagsList(w http.ResponseWriter, r *http.Request, listTags usecase.ListTags) {
	ctx := r.Context()
	iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min"))
	tagData, err := listTags.Run(ctx, iMinCount)
	if err != nil {
		wui.reportError(ctx, w, err)
		return
	}

	user := wui.getUser(ctx)
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}

	tagsList := make([]tagInfo, 0, len(tagData))
	countMap := make(map[int]int)
	baseTagListURL := wui.NewURLBuilder('h')
	for tag, ml := range tagData {
		count := len(ml)
		countMap[count]++
		tagsList = append(
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "list",
			wui.rtConfig.Get(ctx, nil, api.KeyLang),
			wui.rtConfig.GetSiteName(), user)
		if q == nil {
			tagsList,
			tagInfo{tag, baseTagListURL.AppendQuery("tags", tag).String(), count, "", ""})
			rb.bindString("heading", sx.String(wui.rtConfig.GetSiteName()))
		baseTagListURL.ClearQuery()
	}
		} else {
	sort.Slice(tagsList, func(i, j int) bool { return tagsList[i].Name < tagsList[j].Name })

	countList := make([]int, 0, len(countMap))
			var sb strings.Builder
			q.PrintHuman(&sb)
	for count := range countMap {
		countList = append(countList, count)
	}
	sort.Ints(countList)
			rb.bindString("heading", sx.String(sb.String()))
		}
		rb.bindString("query-value", sx.String(q.String()))
		if tzl := q.GetMetaValues(api.KeyTags, false); len(tzl) > 0 {
			sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl)
	for pos, count := range countList {
		countMap[count] = fontSizes[(pos*len(fontSizes))/len(countList)]
	}
	for i := 0; i < len(tagsList); i++ {
		count := tagsList[i].count
			if !sx.IsNil(sxTzl) {
				rb.bindString("tag-zettel", sxTzl)
			}
			if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) {
				rb.bindString("create-tag-zettel", sxNoTzl)
		tagsList[i].Count = strconv.Itoa(count)
		tagsList[i].Size = strconv.Itoa(countMap[count])
	}

	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
			}
		}
		if rzl := q.GetMetaValues(api.KeyRole, false); len(rzl) > 0 {
			sxRzl, sxNoRzl := wui.transformRoleZettelList(ctx, roleZettel, rzl)
			if !sx.IsNil(sxRzl) {
				rb.bindString("role-zettel", sxRzl)
	minCounts := make([]countInfo, 0, len(countList))
	for _, c := range countList {
		sCount := strconv.Itoa(c)
			}
			if !sx.IsNil(sxNoRzl) && wui.canCreate(ctx, user) {
				rb.bindString("create-role-zettel", sxNoRzl)
		minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount})
	}

	wui.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct {
		ListTagsURL string
		MinCounts   []countInfo
			}
		}
		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()
		Tags        []tagInfo
	}{
		ListTagsURL: base.ListTagsURL,
		MinCounts:   minCounts,
		if found {
		Tags:        tagsList,
	})
}

			apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed))
		} else {
			seed = 0
		}
		if len(metaSeq) > 0 {
// MakeSearchHandler creates a new HTTP handler for the use case "search".
func (wui *WebUI) MakeSearchHandler(
	ucSearch usecase.Search,
	getMeta usecase.GetMeta,
	getZettel usecase.GetZettel,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
			rb.bindString("plain-url", sx.String(apiURL.String()))
		ctx := r.Context()
		s := adapter.GetSearch(query, true)
		if s == nil {
			redirectFound(w, r, wui.NewURLBuilder('h'))
			return
		}

			rb.bindString("data-url", sx.String(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String()))
		title := wui.listTitleSearch("Search", s)
		wui.renderMetaList(
			ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) {
				if !s.HasComputedMetaKey() {
					ctx = place.NoEnrichContext(ctx)
				}
				return ucSearch.Run(ctx, s)
			},
			func(offset int) string {
			if wui.canCreate(ctx, user) {
				rb.bindString("create-url", sx.String(wui.createNewURL))
				rb.bindString("seed", sx.Int64(seed))
				return wui.newPageURL('f', query, offset, "offset", "limit")
			})
	}
}

			}
		}
		if rb.err == nil {
// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext) 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, place.ErrNotFound)
			return
			err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env)
		} else {
			err = rb.err
		}
		q := r.URL.Query()
		dir := usecase.ParseZCDirection(q.Get("dir"))
		depth := getIntParameter(q, "depth", 5)
		limit := getIntParameter(q, "limit", 200)
		metaList, err := getContext.Run(ctx, zid, dir, depth, limit)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		metaLinks, err := wui.buildHTMLMetaList(metaList)
		if err != nil {
			adapter.InternalServerError(w, "Build HTML meta list", err)
			return
		}

		depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"}
	}
}

		depthLinks := make([]simpleLink, len(depths))
		depthURL := wui.NewURLBuilder('j').SetZid(zid)
		for i, depth := range depths {
			depthURL.ClearQuery()
func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) {
	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)
			switch dir {
			case usecase.ZettelContextBackward:
			withZettel = wui.prependZettelLink(withZettel, tag, u)
				depthURL.AppendQuery("dir", "backward")
			case usecase.ZettelContextForward:
		} else {
				depthURL.AppendQuery("dir", "forward")
			}
			u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewTag).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, tag)
			withoutZettel = wui.prependZettelLink(withoutZettel, tag, u)
		}
			depthURL.AppendQuery("depth", depth)
			depthLinks[i].Text = depth
			depthLinks[i].URL = depthURL.String()
		}
		var base baseData
		user := wui.getUser(ctx)
	}
	return withZettel, withoutZettel
}

func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []string) (withZettel, withoutZettel *sx.Pair) {
		wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
		wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct {
			Title   string
			InfoURL string
	slices.Reverse(roles)
	for _, role := range roles {
			Depths  []simpleLink
			Start   simpleLink
			Metas   []simpleLink
		}{
			Title:   "Zettel Context",
			InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(),
			Depths:  depthLinks,
		if _, err := roleZettel.Run(ctx, role); err == nil {
			u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyRole, role)
			withZettel = wui.prependZettelLink(withZettel, role, u)
			Start:   metaLinks[0],
			Metas:   metaLinks[1:],
		})
	}
}

		} else {
			u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewRole).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, role)
			withoutZettel = wui.prependZettelLink(withoutZettel, role, u)
		}
	}
func getIntParameter(q url.Values, key string, minValue int) int {
	val, ok := adapter.GetInteger(q, key)
	if !ok || val < 0 {
		return minValue
	}
	return val
	return withZettel, withoutZettel
}

func (wui *WebUI) renderMetaList(
	ctx context.Context,
	w http.ResponseWriter,
	title string,
	s *search.Search,
	ucMetaList func(sorter *search.Search) ([]*meta.Meta, error),
	pageURL func(int) string) {

func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair {
	link := sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.String(u.String())),
		),
		sx.String(name),
	)
	var metaList []*meta.Meta
	var err error
	var prevURL, nextURL string
	if sxZtl != nil {
		sxZtl = sxZtl.Cons(sx.String(", "))
	if lps := wui.rtConfig.GetListPageSize(); lps > 0 {
		if s.GetLimit() < lps {
			s.SetLimit(lps + 1)
		}
	}

		metaList, err = ucMetaList(s)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
	return sxZtl.Cons(link)
}
		if offset := s.GetOffset(); offset > 0 {
			offset -= lps
			if offset < 0 {
				offset = 0
			}
			prevURL = pageURL(offset)
		}
		if len(metaList) >= s.GetLimit() {

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)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction {
		rssConfig.Title = strings.Join(actions[2:], " ")
	}
	data := rssConfig.Marshal(q, ml)
			nextURL = pageURL(s.GetOffset() + lps)
			metaList = metaList[:len(metaList)-1]
		}
	} else {
		metaList, err = ucMetaList(s)
		if err != nil {

	adapter.PrepareHeader(w, rss.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
			wui.reportError(ctx, w, err)
			return
		}
	}
	}
	user := wui.getUser(ctx)
	metas, err := wui.buildHTMLMetaList(metaList)
	if err != nil {
		wui.reportError(ctx, w, err)
		wui.log.Error().Err(err).Msg("unable to write RSS data")
		return
	}
}
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
	wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct {
		Title       string

func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var atomConfig atom.Configuration
	atomConfig.Setup(wui.rtConfig)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction {
		Metas       []simpleLink
		HasPrevNext bool
		HasPrev     bool
		PrevURL     string
		atomConfig.Title = strings.Join(actions[2:], " ")
		HasNext     bool
		NextURL     string
	}{
		Title:       title,
		Metas:       metas,
		HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0,
		HasPrev:     len(prevURL) > 0,
		PrevURL:     prevURL,
		HasNext:     len(nextURL) > 0,
		NextURL:     nextURL,
	})
}
	}
	data := atomConfig.Marshal(q, ml)

func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string {
	if s == nil {
	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)
		return wui.rtConfig.GetSiteName()
	}
	var sb strings.Builder
	sb.WriteString(prefix)
	if s != nil {
		sb.WriteString(": ")
	if err != nil {
		wui.log.Error().Err(err).Msg("unable to write Atom data")
		s.Print(&sb)
	}
	return sb.String()
}

func (wui *WebUI) newPageURL(key byte, query url.Values, offset int, offsetKey, limitKey string) string {
	ub := wui.NewURLBuilder(key)
func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)
	for key, values := range query {
		if key != offsetKey && key != limitKey {
	if tag == "" {
			for _, val := range values {
				ub.AppendQuery(key, val)
			}
		return false
	}
		}
	}
	if offset > 0 {
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, tag)
	if err != nil {
		ub.AppendQuery(offsetKey, strconv.Itoa(offset))
	}
	return ub.String()
		wui.reportError(ctx, w, err)
		return true
}

// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering.
func (wui *WebUI) buildHTMLMetaList(metaList []*meta.Meta) ([]simpleLink, error) {
	}
	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 {
	defaultLang := wui.rtConfig.GetDefaultLang()
	metas := make([]simpleLink, 0, len(metaList))
	for _, m := range metaList {
		var lang string
		if val, ok := m.Get(meta.KeyLang); ok {
			lang = val
		} else {
	role := vals.Get(api.QueryKeyRole)
	if role == "" {
		return false
			lang = defaultLang
		}
		title, _ := m.Get(meta.KeyTitle)
	}
	ctx := r.Context()
		env := encoder.Environment{Lang: lang, Interactive: true}
		htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env)
		if err != nil {
			return nil, err
		}
	z, err := roleZettel.Run(ctx, role)
	if err != nil {
		wui.reportError(ctx, w, err)
		return true
	}
		metas = append(metas, simpleLink{
			Text: htmlTitle,
			URL:  wui.NewURLBuilder('h').SetZid(m.Zid).String(),
	wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID()))
		})
	}
	return metas, nil
	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
77
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20


21
22
23
24
25
26
27


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42



43
44
45
46





47
48





49
50
51
52
53
54
55
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeGetLoginHandler creates a new HTTP handler to display the HTML login view.
func (wui *WebUI) MakeGetLoginHandler() http.HandlerFunc {
// 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) {
		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.GetDefaultLang(), "Login", nil, &base)
	wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct {
	env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil)
	rb.bindString("retry", sx.MakeBoolean(retry))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, env)
		Title string
		Retry bool
	}{
		Title: base.Title,
		Retry: retry,
	}
	if err := rb.err; err != nil {
	})
}

// MakePostLoginHandlerHTML creates a new HTTP handler to authenticate the given user.
func (wui *WebUI) MakePostLoginHandlerHTML(ucAuth usecase.Authenticate) http.HandlerFunc {
		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) {
		if !wui.authz.WithAuth() {
			redirectFound(w, r, wui.NewURLBuilder('/'))
			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, 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)
		redirectFound(w, r, wui.NewURLBuilder('/'))
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}

// MakeGetLogoutHandler creates a new HTTP handler to log out the current user
func (wui *WebUI) MakeGetLogoutHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		wui.clearToken(r.Context(), w)
		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)
			}
		})
	}
}

Changes to 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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20






21
22
23
24
25
26
27
28
29
30

31
32
33
34

35
36

37
38
39
40

41
42
43
44
45
46
47




48
49
50
51
52
53
54
55
56
57
58
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

-
+

-
+




+
+
+


-







-
-
-
-
-
-
+
+
+
+
+
+




-
+


+
-
+

-
+



-
+




+

-
-
-
-
+
+
+
+
+
+
+
+
+
+

+
+
-
+
-
-
-
-
-
-
-
-
-
-




-
+


+
-
+

-
+




+



+
-
-
+
+
+
+
+
+



-
+
+

+
-
+



-
+



-
+


//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
)

// MakeGetRenameZettelHandler creates a new HTTP handler to display the
// HTML rename view of a zettel.
func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc {
func (wui *WebUI) MakeGetRenameZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
	return 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, place.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		m, err := getMeta.Run(ctx, zid)
		z, err := getZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := z.Meta

		if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), format)))
			return
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "rename",
			wui.rtConfig.Get(ctx, nil, api.KeyLang), "Rename Zettel "+m.Zid.String(), user)
		rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.RenameTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)

		}
		user := wui.getUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), user, &base)
		wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct {
			Zid       string
			MetaPairs []meta.Pair
		}{
			Zid:       zid.String(),
			MetaPairs: m.Pairs(true),
		})
	}
}

// MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel.
func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc {
func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		curZid, err := id.Parse(r.URL.Path[1:])
		curZid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, place.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		if err = r.ParseForm(); err != nil {
			wui.log.Trace().Err(err).Msg("unable to read rename zettel form")
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form"))
			return
		}
		formCurZidStr := r.PostFormValue("curzid")
		if formCurZid, err1 := id.Parse(
			r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid {
		if formCurZid, err1 := id.Parse(formCurZidStr); err1 != nil || formCurZid != curZid {
			if err1 != nil {
				wui.log.Trace().Str("formCurzid", formCurZidStr).Err(err1).Msg("unable to parse as zid")
			} else if formCurZid != curZid {
				wui.log.Trace().Zid(formCurZid).Zid(curZid).Msg("zid differ (form/url)")
			}
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form"))
			return
		}
		newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid")))
		formNewZid := strings.TrimSpace(r.PostFormValue("newzid"))
		newZid, err := id.Parse(formNewZid)
		if err != nil {
			wui.reportError(
			wui.reportError(ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", newZid)))
				ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid)))
			return
		}

		if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil {
		if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid))
		wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
	}
}

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
1

2
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 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"net/http"

	"zettelstore.de/z/web/server"
	"zettelstore.de/client.fossil/api"
)

func redirectFound(w http.ResponseWriter, r *http.Request, ub server.URLBuilder) {
	http.Redirect(w, r, ub.String(), http.StatusFound)
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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil/sxeval"
	"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
456
457
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxbuiltins"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) 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(string(s))
			if err != nil {
				return nil, fmt.Errorf("parsing zettel identifier %q: %w", s, err)
			}
			ub := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
			return sx.String(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(string(qs))
			return sx.String(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(string(text)); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				shtml.SymA,
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(shtml.SymAttrHref, sx.String(us)),
					sx.Cons(shtml.SymAttrTarget, sx.String("_blank")),
					sx.Cons(shtml.SymAttrRel, sx.String("noopener 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.String(lang))
	rb.bindString("css-base-url", sx.String(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.String(wui.cssUserURL))
	rb.bindString("title", sx.String(title))
	rb.bindString("home-url", sx.String(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
	rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid))
	rb.bindString("user-zettel-url", sx.String(userZettelURL))
	rb.bindString("user-ident", sx.String(userIdent))
	rb.bindString("login-url", sx.String(wui.loginURL))
	rb.bindString("logout-url", sx.String(wui.logoutURL))
	rb.bindString("list-zettel-url", sx.String(wui.listZettelURL))
	rb.bindString("list-roles-url", sx.String(wui.listRolesURL))
	rb.bindString("list-tags-url", sx.String(wui.listTagsURL))
	if wui.canRefresh(user) {
		rb.bindString("refresh-url", sx.String(wui.refreshURL))
	}
	rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user))
	rb.bindString("search-url", sx.String(wui.searchURL))
	rb.bindString("query-key-query", sx.String(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.String(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.String(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.String(strZid))
	rb.bindString("web-url", sx.String(newURLBuilder('h').SetZid(apiZid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.String(newURLBuilder('e').SetZid(apiZid).String()))
	}
	rb.bindString("info-url", sx.String(newURLBuilder('i').SetZid(apiZid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}
		rb.bindString("version-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		rb.bindString("child-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String()))
		rb.bindString("folge-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canRename(ctx, user, m) {
		rb.bindString("rename-url", sx.String(newURLBuilder('b').SetZid(apiZid).String()))
	}
	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.String(newURLBuilder('d').SetZid(apiZid).String()))
	}
	if val, found := m.Get(api.KeyUselessFiles); found {
		rb.bindString("useless", sx.Cons(sx.String(val), nil))
	}
	queryContext := strZid + " " + api.ContextDirective
	rb.bindString("context-url", sx.String(newURLBuilder('h').AppendQuery(queryContext).String()))
	queryContext += " " + api.FullDirective
	rb.bindString("context-full-url", sx.String(newURLBuilder('h').AppendQuery(queryContext).String()))
	if wui.canRefresh(user) {
		rb.bindString("reindex-url", sx.String(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.String(key), sx.String(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.String(parser.NormalizedSpacedText(z.Meta.GetTitle()))
		link := sx.String(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.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(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) {
	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.String(http.StatusText(code)))
	rb.bindString("message", sx.String(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.String(sl[i]))
	}
	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












1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17

18

19
20
21
22














23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

48
49
50
51

52
53
54
55
56
57

58
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

-
+

-
+




+
+
+






-

-




-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




+
+




-
+


+
-
+

+


+
-
+




+


+

+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
+
+
+
+

+
+




-
+


+
+
+
+

-
+
-
-
+


-
-
+
+
+

-
-
-
-
+
+
+
+
+
+
+
+
+
+




+
+
+
+
+
-
+

-
-
+
+




-
-
-
+
+
+
+
+
+
+
+
+
+

-
+


-
+
-

-
+

+
-
+
+
+




-
+



-
+

-
+



-
+



-
+


-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+

-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-





-
+




+



+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"log"
	"net/http"
	"sync"
	"time"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"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/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/template"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"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
	place    webuiPlace
	box      webuiBox
	policy   auth.Policy

	evalZettel *usecase.Evaluate
	templateCache map[id.Zid]*template.Template

	mxCache       sync.RWMutex
	templateCache map[id.Zid]sxeval.Expr

	tokenLifetime time.Duration
	cssBaseURL    string
	stylesheetURL string
	cssUserURL    string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	refreshURL    string
	withAuth      bool
	loginURL      string
	logoutURL     string
	searchURL     string
	createNewURL  string
}

type webuiPlace 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
	CanDeleteZettel(ctx context.Context, zid id.Zid) bool

	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(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
	AllowRenameZettel(context.Context, id.Zid) bool
	CanDeleteZettel(context.Context, id.Zid) bool
}

// New creates a new WebUI struct.
func New(ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr place.Manager, pol auth.Policy) *WebUI {
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,
		place:    mgr,
		box:      mgr,
		policy:   pol,

		evalZettel: evalZettel,

		templateCache: make(map[id.Zid]sxeval.Expr, 32),

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration),
		stylesheetURL: ab.NewURLBuilder('z').SetZid(
		cssBaseURL:    ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(),
			id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery(
			"_part", "content").String(),
		cssUserURL:    ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(),
		homeURL:       ab.NewURLBuilder('/').String(),
		listZettelURL: ab.NewURLBuilder('h').String(),
		listRolesURL:  ab.NewURLBuilder('h').AppendQuery("_l", "r").String(),
		listTagsURL:   ab.NewURLBuilder('h').AppendQuery("_l", "t").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:      ab.NewURLBuilder('a').String(),
		searchURL:     ab.NewURLBuilder('f').String(),
	}
	wui.observe(place.UpdateInfo{Place: mgr, Reason: place.OnReload, Zid: id.Invalid})
		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
}

var (
	symDetail     = sx.MakeSymbol("DETAIL")
	symMetaHeader = sx.MakeSymbol("META-HEADER")
)

func (wui *WebUI) observe(ci place.UpdateInfo) {
func (wui *WebUI) observe(ci box.UpdateInfo) {
	wui.mxCache.Lock()
	if ci.Reason == place.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()
}

func (wui *WebUI) cacheSetTemplate(zid id.Zid, t *template.Template) {

	wui.mxZettelBinding.Lock()
	if ci.Reason == box.OnReload || wui.dag.HasVertex(ci.Zid) {
		wui.zettelBinding = nil
		wui.dag = nil
	}
	wui.mxZettelBinding.Unlock()
}

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()
	if found {
	return t, ok
		return expr
	}
	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.place.CanCreateZettel(ctx)
	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.place.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.place.AllowRenameZettel(ctx, m.Zid)
	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.place.CanDeleteZettel(ctx, m.Zid)
	return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid)
}

func (wui *WebUI) getTemplate(
func (wui *WebUI) canRefresh(user *meta.Meta) bool {
	ctx context.Context, templateID id.Zid) (*template.Template, error) {
	if t, ok := wui.cacheGetTemplate(templateID); ok {
		return t, nil
	}
	realTemplateZettel, err := wui.place.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 baseData struct {
	Lang           string
	MetaHeader     string
	StylesheetURL  string
	Title          string
	HomeURL        string
	WithUser       bool
	WithAuth       bool
	UserIsValid    bool
	UserZettelURL  string
	UserIdent      string
	UserLogoutURL  string
	LoginURL       string
	ListZettelURL  string
	ListRolesURL   string
	ListTagsURL    string
	CanCreate      bool
	NewZettelURL   string
	NewZettelLinks []simpleLink
	SearchURL      string
	Content        string
	FooterHTML     string
}

func (wui *WebUI) makeBaseData(
	ctx context.Context, lang, title string, user *meta.Meta, data *baseData) {
	var (
		newZettelLinks []simpleLink
		userZettelURL  string
		userIdent      string
		userLogoutURL  string
	)
	canCreate := wui.canCreate(ctx, user)
	if canCreate {
		newZettelLinks = wui.fetchNewTemplates(ctx, user)
	}
	userIsValid := user != nil
	if userIsValid {
		userZettelURL = wui.NewURLBuilder('h').SetZid(user.Zid).String()
		userIdent = user.GetDefault(meta.KeyUserID, "")
		userLogoutURL = wui.NewURLBuilder('a').SetZid(user.Zid).String()
	}

	data.Lang = lang
	data.StylesheetURL = wui.stylesheetURL
	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.UserLogoutURL = userLogoutURL
	data.LoginURL = wui.loginURL
	data.ListZettelURL = wui.listZettelURL
	data.ListRolesURL = wui.listRolesURL
	data.ListTagsURL = wui.listTagsURL
	data.CanCreate = canCreate
	data.NewZettelLinks = newZettelLinks
	data.SearchURL = wui.searchURL
	data.FooterHTML = wui.rtConfig.GetFooterHTML()
}

// htmlAttrNewWindow eturns 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\" ref=\"noopener noreferrer\""
	}
	return ""
	return wui.policy.CanRefresh(user)
}

func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink {
	ctx = place.NoEnrichContext(ctx)
	menu, err := wui.place.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}

	zn := parser.ParseZettel(menu, "", wui.rtConfig)
	refs := collect.Order(zn)
	result := make([]simpleLink, 0, len(refs))
	for _, ref := range refs {
		zid, err := id.Parse(ref.URL.Path)
		if err != nil {
			continue
		}
		m, err := wui.place.GetMeta(ctx, zid)
		if err != nil {
			continue
		}
		if !wui.policy.CanRead(user, m) {
			continue
		}
		title := config.GetTitle(m, wui.rtConfig)
		astTitle := parser.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk)
		env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)}
		menuTitle, err := adapter.FormatInlines(astTitle, "html", &env)
		if err != nil {
			menuTitle, err = adapter.FormatInlines(astTitle, "text", nil)
			if err != nil {
				menuTitle = title
			}
		}
		result = append(result, simpleLink{
			Text: menuTitle,
			URL:  wui.NewURLBuilder('g').SetZid(m.Zid).String(),
		})
	}
	return result
}

func (wui *WebUI) renderTemplate(
func (wui *WebUI) getSimpleHTMLEncoder(lang string) *htmlGenerator {
	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 {
		log.Printf("%v: %v", text, err)
	}
	user := wui.getUser(ctx)
	var base baseData
	wui.makeBaseData(ctx, meta.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 {
		adapter.InternalServerError(w, "Unable to get base template", err)
		return
	}
	return wui.createGenerator(wui, lang)
}
	t, err := wui.getTemplate(ctx, templateID)
	if err != nil {
		adapter.InternalServerError(w, "Unable to get template", err)
		return
	}
	if user := wui.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 {
		base.Content = content.String()
		w.Header().Set(adapter.ContentType, "text/html; charset=utf-8")
		w.WriteHeader(code)
		err = bt.Render(w, base)
	}
	if err != nil {
		log.Println("Unable to render template", err)
	}
}

func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) }

// 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) server.URLBuilder { return wui.ab.NewURLBuilder(key) }
func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) }

func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context {
	return wui.ab.ClearToken(ctx, w)
}

func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) {
	wui.ab.SetToken(w, token, wui.tokenLifetime)
}

func (wui *WebUI) prepareAndWriteHeader(w http.ResponseWriter, statusCode int) {
	h := adapter.PrepareHeader(w, "text/html; charset=utf-8")
	h.Set("Content-Security-Policy", "default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'")
	h.Set("Permissions-Policy", "payment=(), interest-cohort=()")
	h.Set("Referrer-Policy", "no-referrer")
	h.Set("X-Content-Type-Options", "nosniff")
	if !wui.debug {
		h.Set("X-Frame-Options", "sameorigin")
	}
	w.WriteHeader(statusCode)
}

Added 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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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/client.fossil/api"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)

const (
	UnknownMIME  = "application/octet-stream"
	mimeGIF      = "image/gif"
	mimeHTML     = "text/html; charset=utf-8"
	mimeJPEG     = "image/jpeg"
	mimeMarkdown = "text/markdown; charset=utf-8"
	PlainText    = "text/plain; charset=utf-8"
	mimePNG      = "image/png"
	SXPF         = PlainText
	mimeWEBP     = "image/webp"
)

var encoding2mime = map[api.EncodingEnum]string{
	api.EncoderHTML:  mimeHTML,
	api.EncoderMD:    mimeMarkdown,
	api.EncoderSz:    SXPF,
	api.EncoderSHTML: SXPF,
	api.EncoderText:  PlainText,
	api.EncoderZmk:   PlainText,
}

// MIMEFromEncoding returns the MIME encoding for a given zettel encoding
func MIMEFromEncoding(enc api.EncodingEnum) string {
	if m, found := encoding2mime[enc]; found {
		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.SyntaxNone:     "",
	meta.SyntaxPlain:    PlainText,
	meta.SyntaxPNG:      mimePNG,
	meta.SyntaxSVG:      "image/svg+xml",
	meta.SyntaxSxn:      SXPF,
	meta.SyntaxText:     PlainText,
	meta.SyntaxTxt:      PlainText,
	meta.SyntaxWebp:     mimeWEBP,
	meta.SyntaxZmk:      "text/x-zmk; charset=utf-8",

	// Additional syntaxes that are parsed as plain text.
	"js":  "text/javascript; charset=utf-8",
	"pdf": "application/pdf",
	"xml": "text/xml; charset=utf-8",
}

// MIMEFromSyntax returns a MIME encoding for a given syntax value.
func MIMEFromSyntax(syntax string) string {
	if mt, found := syntax2mime[syntax]; found {
		return mt
	}
	return UnknownMIME
}

var mime2syntax = map[string]string{
	mimeGIF:         meta.SyntaxGif,
	mimeJPEG:        meta.SyntaxJPEG,
	mimePNG:         meta.SyntaxPNG,
	mimeWEBP:        meta.SyntaxWebp,
	"text/html":     meta.SyntaxHTML,
	"text/markdown": meta.SyntaxMarkdown,
	"text/plain":    meta.SyntaxText,

	// Additional syntaxes
	"application/pdf": "pdf",
	"text/javascript": "js",
}

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 zettel.IsBinary(data) {
			return "binary"
		}
	}
	return "plain"
}

Added web/content/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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/content"

	_ "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 TestSupportedSyntax(t *testing.T) {
	for _, syntax := range parser.GetSyntaxes() {
		mt := content.MIMEFromSyntax(syntax)
		if mt == content.UnknownMIME {
			t.Errorf("No MIME type registered for syntax %q", syntax)
			continue
		}

		newSyntax := content.SyntaxFromMIME(mt, nil)
		pinfo := parser.Get(newSyntax)
		if pinfo == nil {
			t.Errorf("MIME type for syntax %q is %q, but this has no corresponding syntax", syntax, mt)
			continue
		}
	}
}

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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the Zettelstore web service.
package impl

import (
	"context"
	"net"
	"net/http"
	"time"
)

// Server timeout values
const (
	shutdownTimeout = 5 * time.Second
	readTimeout     = 5 * time.Second
	writeTimeout    = 10 * time.Second
	idleTimeout     = 120 * time.Second
)

// httpServer is a HTTP server.
type httpServer struct {
	http.Server
	waitStop chan struct{}
}

// 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,

		// See: https://blog.cloudflare.com/exposing-go-on-the-internet/
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
		IdleTimeout:  idleTimeout,
	}
	srv.waitStop = make(chan struct{})
}

// 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
66
67
68
69
70
71
72
73

74
75
76
77

78
66
67
68
69
70
71
72

73
74
75
76

77
78







-
+



-
+

	}

	go func() { srv.Serve(ln) }()
	return nil
}

// Stop the web server.
func (srv *httpServer) Stop() error {
func (srv *httpServer) Stop() {
	ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
	defer cancel()

	return srv.Shutdown(ctx)
	srv.Shutdown(ctx)
}

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
52
53
54
55
56
57
58



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



1

2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

39
40
41
42
43
44
45

46
47
48
49
50
51
52
53


54
55
56


57
58
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

-
+

-
+




+
+
+










+

-
+

+



+
+







-
+

+
+



-
+







-
-
+
+

-
-
+
+




-
-
-
-
-
+
+
+

-
-
+
+

-
-
+
+











-
+




+
+
-
+
+
+
+




+
+
+
+






-
-
-
-
-
-
-
-
-
-
-
-
-


-
+



-
+









-
-
-
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the Zettelstore web service.
package impl

import (
	"context"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/meta"
	"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
}

// New creates a new web server.
func New(listenAddr, urlPrefix string, persistentCookie, secureCookie bool, auth auth.TokenManager) server.Server {
func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server {
	srv := myServer{
		log:              log,
		baseURL:          baseURL,
		persistentCookie: persistentCookie,
		secureCookie:     secureCookie,
	}
	srv.router.initializeRouter(urlPrefix, auth)
	srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth)
	srv.server.initializeHTTPServer(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, httpMethod string, handler http.Handler) {
	srv.router.addListRoute(key, httpMethod, handler)
func (srv *myServer) AddListRoute(key byte, method server.Method, handler http.Handler) {
	srv.router.addListRoute(key, method, handler)
}
func (srv *myServer) AddZettelRoute(key byte, httpMethod string, handler http.Handler) {
	srv.router.addZettelRoute(key, httpMethod, handler)
func (srv *myServer) AddZettelRoute(key byte, method server.Method, handler http.Handler) {
	srv.router.addZettelRoute(key, method, handler)
}
func (srv *myServer) SetUserRetriever(ur server.UserRetriever) {
	srv.router.ur = ur
}
func (srv *myServer) GetUser(ctx context.Context) *meta.Meta {
	if data := srv.GetAuthData(ctx); data != nil {
		return data.User
	}
	return nil

func (srv *myServer) GetURLPrefix() string {
	return srv.router.urlPrefix
}
func (srv *myServer) NewURLBuilder(key byte) server.URLBuilder {
	return &URLBuilder{router: &srv.router, key: key}
func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder {
	return api.NewURLBuilder(srv.GetURLPrefix(), key)
}
func (srv *myServer) GetURLPrefix() string {
	return srv.router.urlPrefix
func (srv *myServer) NewURLBuilderAbs(key byte) *api.URLBuilder {
	return api.NewURLBuilder(srv.baseURL, key)
}

const sessionName = "zsession"

func (srv *myServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) {
	cookie := http.Cookie{
		Name:     sessionName,
		Value:    string(token),
		Path:     srv.GetURLPrefix(),
		Secure:   srv.secureCookie,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
		SameSite: http.SameSiteLaxMode,
	}
	if srv.persistentCookie && d > 0 {
		cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC()
	}
	srv.log.Debug().Bytes("token", token).Msg("SetToken")
	if v := cookie.String(); v != "" {
	http.SetCookie(w, &cookie)
		w.Header().Add("Set-Cookie", v)
		w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`)
		w.Header().Add("Vary", "Cookie")
	}
}

// ClearToken invalidates the session cookie by sending an empty one.
func (srv *myServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context {
	if authData := server.GetAuthData(ctx); authData == nil {
		// No authentication data stored in session, nothing to do.
		return ctx
	}
	if w != nil {
		srv.SetToken(w, nil, 0)
	}
	return updateContext(ctx, nil, nil)
}

// GetAuthData returns the full authentication data from the context.
func (srv *myServer) GetAuthData(ctx context.Context) *server.AuthData {
	data, ok := ctx.Value(ctxKeySession).(*server.AuthData)
	if ok {
		return data
	}
	return nil
}

type ctxKeyTypeSession struct{}

var ctxKeySession ctxKeyTypeSession

func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context {
	if data == nil {
		return context.WithValue(ctx, ctxKeySession, &server.AuthData{User: user})
		return context.WithValue(ctx, server.CtxKeySession, &server.AuthData{User: user})
	}
	return context.WithValue(
		ctx,
		ctxKeySession,
		server.CtxKeySession,
		&server.AuthData{
			User:    user,
			Token:   data.Token,
			Now:     data.Now,
			Issued:  data.Issued,
			Expires: data.Expires,
		})
}

func (srv *myServer) SetDebug()   { srv.server.SetDebug() }
func (srv *myServer) Run() error  { return srv.server.Run() }
func (srv *myServer) Stop() error { return srv.server.Stop() }
func (srv *myServer) SetDebug()  { srv.server.SetDebug() }
func (srv *myServer) Run() error { return srv.server.Run() }
func (srv *myServer) Stop()      { srv.server.Stop() }

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
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
1

2
3

4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

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) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package impl provides the Zettelstore web service.
package impl

import (
	"io"
	"net/http"
	"regexp"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
)

type (
	methodHandler map[string]http.Handler
	routingTable  map[byte]methodHandler
	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
}

// initializeRouter creates a new, empty router with the given root handler.
func (rt *httpRouter) initializeRouter(urlPrefix string, auth auth.TokenManager) {
func (rt *httpRouter) initializeRouter(log *logger.Logger, urlPrefix string, maxRequestSize int64, auth auth.TokenManager) {
	rt.log = log
	rt.urlPrefix = urlPrefix
	rt.auth = auth
	rt.minKey = 255
	rt.maxKey = 0
	rt.reURL = regexp.MustCompile("^$")
	rt.mux = http.NewServeMux()
	rt.listTable = make(routingTable)
	rt.maxReqSize = maxRequestSize
	rt.zettelTable = make(routingTable)
}

func (rt *httpRouter) addRoute(key byte, httpMethod string, handler http.Handler, table routingTable) {
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
		}
		if rt.maxKey < key {
			rt.maxKey = key
		}
		rt.reURL = regexp.MustCompile(
			"^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:([0-9]{14})/?)?)?)$")
	}

	mh, hasKey := table[key]
	if !hasKey {
		mh = make(methodHandler)
	mh := table[key]
	if mh == nil {
		mh = new(methodHandler)
		table[key] = mh
	}
	mh[httpMethod] = handler
	if httpMethod == http.MethodGet {
		if _, hasHead := table[key][http.MethodHead]; !hasHead {
			table[key][http.MethodHead] = handler
	mh[method] = handler
	if method == server.MethodGet {
		if prevHandler := mh[server.MethodHead]; prevHandler == nil {
			mh[server.MethodHead] = handler
		}
	}
}

// addListRoute adds a route for the given key and HTTP method to work with a list.
func (rt *httpRouter) addListRoute(key byte, httpMethod string, handler http.Handler) {
	rt.addRoute(key, httpMethod, handler, rt.listTable)
func (rt *httpRouter) addListRoute(key byte, method server.Method, handler http.Handler) {
	rt.addRoute(key, method, handler, &rt.listTable)
}

// addZettelRoute adds a route for the given key and HTTP method to work with a zettel.
func (rt *httpRouter) addZettelRoute(key byte, httpMethod string, handler http.Handler) {
	rt.addRoute(key, httpMethod, handler, rt.zettelTable)
func (rt *httpRouter) addZettelRoute(key byte, method server.Method, handler http.Handler) {
	rt.addRoute(key, method, handler, &rt.zettelTable)
}

// Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics.
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 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", ri)
		}
	}()

	var withDebug bool
	if msg := rt.log.Debug(); msg.Enabled() {
		withDebug = true
		w = &traceResponseWriter{original: w}
		msg.Str("method", r.Method).Str("uri", r.RequestURI).HTTPIP(r).Msg("ServeHTTP")
	}

	if prefixLen := len(rt.urlPrefix); prefixLen > 1 {
		if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix {
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			if withDebug {
				rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP/prefix")
			}
			return
		}
		r.URL.Path = r.URL.Path[prefixLen-1:]
	}
	r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize)
	match := rt.reURL.FindStringSubmatch(r.URL.Path)
	if len(match) == 3 {
		key := match[1][0]
	if len(match) != 3 {
		rt.mux.ServeHTTP(w, rt.addUserContext(r))
		if withDebug {
			rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("match other")
		}
		return
	}
	if withDebug {
		rt.log.Debug().Str("key", match[1]).Str("zid", match[2]).Msg("path match")
	}

	key := match[1][0]
		table := rt.zettelTable
		if match[2] == "" {
			table = rt.listTable
		}
		if mh, ok := table[key]; ok {
			if handler, ok := mh[r.Method]; ok {
				r.URL.Path = "/" + match[2]
				handler.ServeHTTP(w, rt.addUserContext(r))
				return
	var mh *methodHandler
	if match[2] == "" {
		mh = rt.listTable[key]
	} else {
		mh = rt.zettelTable[key]
	}
	method, ok := mapMethod[r.Method]
	if ok && mh != nil {
		if handler := mh[method]; handler != nil {
			r.URL.Path = "/" + match[2]
			handler.ServeHTTP(w, rt.addUserContext(r))
			if withDebug {
				rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP")
			}
			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
			return
		}
	}
	rt.mux.ServeHTTP(w, rt.addUserContext(r))
	http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	if withDebug {
		rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match")
	}
}

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.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.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)
167
168
169
170
171
172
173















224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
	const prefix = "Bearer "
	// RFC 2617, subsection 1.2 defines the scheme token as case-insensitive.
	if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
		return nil
	}
	return []byte(auth[len(prefix):])
}

type traceResponseWriter struct {
	original   http.ResponseWriter
	statusCode int
}

func (w *traceResponseWriter) Header() http.Header         { return w.original.Header() }
func (w *traceResponseWriter) Write(p []byte) (int, error) { return w.original.Write(p) }
func (w *traceResponseWriter) WriteHeader(statusCode int) {
	w.statusCode = statusCode
	w.original.WriteHeader(statusCode)
}
func (w *traceResponseWriter) WriteString(s string) (int, error) {
	return io.WriteString(w.original, s)
}

Deleted web/server/impl/urlbuilder.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package impl provides the Zettelstore web service.
package impl

import (
	"net/url"
	"strings"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/web/server"
)

type urlQuery struct{ key, val string }

// URLBuilder should be used to create zettelstore URLs.
type URLBuilder struct {
	router   *httpRouter
	key      byte
	path     []string
	query    []urlQuery
	fragment string
}

// Clone an URLBuilder
func (ub *URLBuilder) Clone() server.URLBuilder {
	cpy := new(URLBuilder)
	cpy.key = ub.key
	if len(ub.path) > 0 {
		cpy.path = make([]string, 0, len(ub.path))
		cpy.path = append(cpy.path, ub.path...)
	}
	if len(ub.query) > 0 {
		cpy.query = make([]urlQuery, 0, len(ub.query))
		cpy.query = append(cpy.query, ub.query...)
	}
	cpy.fragment = ub.fragment
	return cpy
}

// SetZid sets the zettel identifier.
func (ub *URLBuilder) SetZid(zid id.Zid) server.URLBuilder {
	if len(ub.path) > 0 {
		panic("Cannot add Zid")
	}
	ub.path = append(ub.path, zid.String())
	return ub
}

// AppendPath adds a new path element
func (ub *URLBuilder) AppendPath(p string) server.URLBuilder {
	ub.path = append(ub.path, p)
	return ub
}

// AppendQuery adds a new query parameter
func (ub *URLBuilder) AppendQuery(key, value string) server.URLBuilder {
	ub.query = append(ub.query, urlQuery{key, value})
	return ub
}

// ClearQuery removes all query parameters.
func (ub *URLBuilder) ClearQuery() server.URLBuilder {
	ub.query = nil
	ub.fragment = ""
	return ub
}

// SetFragment stores the fragment
func (ub *URLBuilder) SetFragment(s string) server.URLBuilder {
	ub.fragment = s
	return ub
}

// String produces a string value.
func (ub *URLBuilder) String() string {
	var sb strings.Builder

	sb.WriteString(ub.router.urlPrefix)
	if ub.key != '/' {
		sb.WriteByte(ub.key)
	}
	for _, p := range ub.path {
		sb.WriteByte('/')
		sb.WriteString(url.PathEscape(p))
	}
	if len(ub.fragment) > 0 {
		sb.WriteByte('#')
		sb.WriteString(ub.fragment)
	}
	for i, q := range ub.query {
		if i == 0 {
			sb.WriteByte('?')
		} else {
			sb.WriteByte('&')
		}
		sb.WriteString(q.key)
		sb.WriteByte('=')
		sb.WriteString(url.QueryEscape(q.val))
	}
	return sb.String()
}

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
46


47
48
49
50
51
52
53
54
55
56


57
58
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
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) 2020-2021 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of zettelstore.
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package server provides the Zettelstore web service.
package server

import (
	"context"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// URLBuilder builds URLs.
type URLBuilder interface {
// UserRetriever allows to retrieve user data based on a given zettel identifier.
type UserRetriever interface {
	// Clone an URLBuilder
	Clone() URLBuilder

	GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error)
}
	// SetZid sets the zettel identifier.
	SetZid(zid id.Zid) URLBuilder

	// AppendPath adds a new path element
	AppendPath(p string) URLBuilder
// Method enumerates the allowed HTTP methods.
type Method uint8

	// AppendQuery adds a new query parameter
	AppendQuery(key, value string) URLBuilder

// Values for method type
const (
	// ClearQuery removes all query parameters.
	ClearQuery() URLBuilder

	// SetFragment stores the fragment
	SetFragment(s string) URLBuilder

	MethodGet Method = iota
	MethodHead
	MethodPost
	MethodPut
	MethodMove
	MethodDelete
	// String produces a string value.
	String() string
}

	MethodLAST // must always be the last one
)
// 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)
}

// Router allows to state routes for various URL paths.
type Router interface {
	Handle(pattern string, handler http.Handler)
	AddListRoute(key byte, httpMethod string, handler http.Handler)
	AddZettelRoute(key byte, httpMethod string, handler http.Handler)
	AddListRoute(key byte, method Method, handler http.Handler)
	AddZettelRoute(key byte, method Method, handler http.Handler)
	SetUserRetriever(ur UserRetriever)
}

// Builder allows to build new URLs for the web service.
type Builder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) URLBuilder
	NewURLBuilder(key byte) *api.URLBuilder
	NewURLBuilderAbs(key byte) *api.URLBuilder
}

// Auth is.
// Auth is the authencation interface.
type Auth interface {
	GetUser(context.Context) *meta.Meta
	// SetToken sends the token to the client.
	SetToken(w http.ResponseWriter, token []byte, d time.Duration)

	// ClearToken invalidates the session cookie by sending an empty one.
	ClearToken(ctx context.Context, w http.ResponseWriter) context.Context

	// GetAuthData returns the full authentication data from the context.
	GetAuthData(ctx context.Context) *AuthData
}

// AuthData stores all relevant authentication data for a context.
type AuthData struct {
	User    *meta.Meta
	Token   []byte
	Now     time.Time
	Issued  time.Time
	Expires time.Time
}

// GetAuthData returns the full authentication data from the context.
func GetAuthData(ctx context.Context) *AuthData {
	if ctx != nil {
		data, ok := ctx.Value(CtxKeySession).(*AuthData)
		if ok {
			return data
		}
	}
	return nil
}

// GetUser returns the metadata of the current user, or nil if there is no one.
func GetUser(ctx context.Context) *meta.Meta {
	if data := GetAuthData(ctx); data != nil {
		return data.User
	}
	return nil
}

// CtxKeyTypeSession is just an additional type to make context value retrieval unambiguous.
type CtxKeyTypeSession struct{}

// CtxKeySession is the key value to retrieve Authdata
var CtxKeySession CtxKeyTypeSession

// AuthBuilder is a Builder that also allows to execute authentication functions.
type AuthBuilder interface {
	Auth
	Builder
}

// Server is the main web server for accessing Zettelstore via HTTP.
type Server interface {
	Router
	Auth
	Builder

	SetDebug()
	Run() error
	Stop() error
	Stop()
}

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






































1
2
3
4



5
6
7
8
9
10
11
12
13
14
15

16
17
18
19


20
21
22

23
24

25
26



27



28
29
30

31
32
33

34
35
36
37
38
39

40
41


42
43
44
45




46
47
48
49
50
51

52
53
54
55
56
57

58
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
-
+



-
-
-
+
+
+
+
+
+
+

+
+

-
+
+


-
-
+
+

-
+

-
+

-
-
-
+
-
-
-
+
+

-
+


-
+





-
+

-
-
+
+


-
-
-
-






-
+





-
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
# How to build the Zettelstore
# How to build Zettelstore
## Prerequisites
You must install the following software:

* A current, supported [release of Go](https://golang.org/doc/devel/release.html),
* [golint](https://github.com/golang/lint|golint),
* [Fossil](https://fossil-scm.org/).
* A current, supported [release of Go](https://go.dev/doc/devel/release),
* [staticcheck](https://staticcheck.io/),
* [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow),
* [unparam](https://mvdan.cc/unparam),
* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),
* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).

See folder `docs/development` (a zettel box) for details.

## Clone the repository
Most of this is covered by the excellent Fossil documentation.
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/fossil</tt>.
1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossil/zettelstore.fossil`.
   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/fossil/zettelstore.fossil`.
1. Open development: `fossil open $HOME/fossils/zettelstore.fossil`.

(If you are not able to use Fossil, you could try the Git 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.

`COMMAND` is one of:
Some important `COMMAND`s are:

* `build`: builds the software with correct version information and places it
  into a freshly created directory <tt>bin</tt>.
* `build`: builds the software with correct version information and puts it
  into a freshly created directory `bin`.
* `check`: checks the current state of the working directory to be ready for
  release (or commit).
* `release`: executes `check` command and if this was successful, builds the
  software for various platforms, and creates ZIP files for each executable.
  Everything is placed in the directory <tt>releases</tt>.
* `clean`: removes the directories <tt>bin</tt> and <tt>releases</tt>.
* `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 worspace.
* `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://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are
also managed by Fossil.
Depending on your development setup, some error messages might occur.

If the error message mentions an environment variable called `GOVCS` you should
set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous
to `GOVCS=*:all`).
Since the Go build system is coupled with Git and some special platforms, you
allow ot to download a Fossil repository from the host `zettelstore.de`.
The build tool set `GOVCS` to the right value, but you may use other `go`
commands that try to download a Fossil repository.

On some operating systems, namely Termux on Android, an error message might
state that an user cannot be determined (`cannot determine user`).
In this case, Fossil is allowed to download the repository, but cannot
associate it with an user name.
Set the environment variable `USER` to any user name, like:
`USER=nobody go run tools/build.go build`.

Changes to www/changes.wiki.

1
2


































































































































3
4
















































































































































































































































































































































































































































































































































































































































































































































































































































5
6

7
8
9
10



11
12
13


14
15
16
17
18
19
20
21

22
23
24
25
26



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

43
44
45
46


47
48

49
50
51

52
53
54
55
56
57
58

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
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


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



+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+

-
-
-
+
+
+

-
-
+
+







-
+
-


-
-
+
+
+















-
+


-
-
+
+

-
+


-
+






-
+




-
-
-
+
+
+




-
+

-
+


-
-
+
+

-
+
+


-
+



-
-
+
+


-
+


-
-
+
+
+

-
-
-
-
+
+
+
+

-
+
+

-
-
-
+
+
+

-
+
+


-
+


-
-
-
+
+
+
+

-
-
-
+
+
+
+

-
-
-
-
+
+
+
+

-
-
-
-
-
-
+
+
+
+
+
+
+

-
-
-
-
-
-
+
+
+
+
+
+
+
+

-
+
+

-
+
+

-
+



-
+
+

-
+
+

-
+
+

-
+

-
-
+
+




-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
+
+
+

-
+
+




-
-
-
-
+
+
+
+
+
+


-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+
+


-
+


-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+

-
-
-
-
-
-
+
+
+
+
+
+
+
+
+

-
+
+

-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
-
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+


-
-
-
-
+
+
+
+

-
+


-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+



-
+
+

-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+

-
-
+
+
+


-
+


-
+
+
+

-
+





-
+

-
-
-
+
+
+
+
+

-
-
+
+
+

-
+


-
<title>Change Log</title>

<a id="0_18"></a>
<h2>Changes for Version 0.18.0 (pending)</h2>
  *  Remove Sx macro <code>defunconst</code>. Use <code>defun</code> instead.
     (breaking: webui)
  *  Update Sx prelude: make macros more robust / more general. This might
     break your code in the future.
     (minor: webui)
  *  Add computed zettel &ldquo;Zettelstore Memory&rdquo; with zettel
     identifier <code>00000000000008</code>. It shows some statistics about
     memory usage.
     (minor: webui)

<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 abbrevated 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 occured durng rendering the original error page.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_14"></a>
<h2>Changes for Version 0.0.14 (pending)</h2>
<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 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 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 <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 <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 id="0_9"></a>
<h2>Changes for Version 0.9.0 (2022-12-12)</h2>
  *  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 <code>.webp</code> as an binary
     image file format.
     (minor)
  *  Allow to specify service specific log level via statup configuration and
     via command line.
     (minor)
  *  Allow to specify a zettel to serve footer content via runtime
     comfiguration <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 <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 <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
     (<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 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.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<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 <code>all-tags</code> and <code>computed-tags</code>.
     Deprecated in version 0.7.0.
     (breaking: zettelmarkup, api, webui)
  *  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 <code>insecure-html</code>.
     (breaking: markup)
  *  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.
     (minor: api, webui)
  *  Add support for creating an Atom 1.0 feed using a query action.
     (minor: api, webui)
  *  Ignore entities with code point that is not allowed in HTML.
     (minor: zettelmarkup)
  *  Enhance distribution of tag sizes when show a tag cloud.
     (minor: webui)
  *  Warn user if zettelstore listens non-locally, but no authentication is
     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 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
     <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: <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 <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 <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 <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 <code>pikchr</code>, a markup language for diagrams in
     technical documentation.
     (major)
  *  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
     <code>/assests/</code>.
     (minor: server)
  *  Add support for metadata key <code>created</code>, a timestamp 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 <code>author</code>. It will be displayed on
     a zettel, if set.
     (minor: webui)
  *  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 <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 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
     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
     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 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)

<h2>Changes for Version 0.5.0 (2022-07-29)</h2>
  *  Removed zettel syntax &ldquo;draw&rdquo;. The new default syntax for
     inline zettel is now &ldquo;text&rdquo;. A drawing can now be made by
     using the &ldquo;evaluation block&rdquo; syntax (see below) by setting
     the generic attribute to &ldquo;draw&rdquo;.
     (breaking: zettelmarkup, api, webui)
  *  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 <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.
     (minor: zettelmarkup)
  *  Add initial support for a TeX-like math-mode to Zettelmarkup (both block-
     and inline-structured elements). Currently, support only the syntax,
     but WebUI does not render these elements in a special way.
     (minor: zettelmarkup)
  *  For block-structured elements, attributes may now span more than one line.
     If a line ending occurs within a quoted attribute value, the line ending
     characters are part of the attributes value.
     (minor: zettelmarkup)
  *  Zettel 00000000029000 acts as a map of zettel roles to identifier of
     zettel that are included as additional CSS. Allows to display zettel
     differently depending on their role. Use case: slides that are processed
     by Zettel Presenter will use the same CSS if they are rendered by
     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)
  *  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 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 <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
     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
     client software.
     (breaking: api)
  *  Remove API call to retrieve all links of an zettel. This can be done more
     easily on the client side by traversing the ZJSON encoding of a zettel.
     (breaking: api)
  *  ZJSON will encode metadata value as pairs of a metadata type and metadata
     value. This allows a client to decode the associated value more easily.
     (minor: api)
  *  A sequence of inline-structured elements can be marked, not just a point
     in the Zettelmarkup text.
     (minor: zettelmarkup)
  *  Metadata keys with suffix <kbd>-title</kbd> force their value to be
     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 <code>no-index</code> 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.
     (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.
     (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 id="0_3"></a>
<h2>Changes for Version 0.3 (2022-02-09)</h2>
  *  Zettel files with extension <code>.meta</code> are now treated as content
     files. Previoulsy, 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 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 <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
     <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 <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 <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)
  *  <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 <code>00</code> by using the query
     <code>id=!&lt;00</code>.
     (minor: api, webui)
  *  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.
     (minor: api)
  *  If there is nothing to return, use HTTP status code 204, instead of 200 +
     <code>Content-Length: 0</code>.
     (minor: api)
  *  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.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<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 <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
     <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
     <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]
     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
     (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 <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 <code>/m</code> to retrieve only the metadata of
     a zettel.
     (minor: api)
  *  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)
  *  Fix: do not allow control characters in JSON-based creating/updating API.
     Otherwise, the created / updated zettel might not be parseable by the
     software (but still by a human). In certain cases, even the WebUI might be
     affected.
     (minor: api, webui)
  *  Fix: when a very long word (longer than width of browser window) is given,
     still allow to scroll horizontally.
     (minor: webui)
  *  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 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 <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.
     (minor: api, webui; possibly breaking)
  *  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
     <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
     <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 <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 addtion, <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 <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 <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 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 <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
     <code>list-page-size</code> is removed. If you still specify it, it will
     be ignored.
     (major: webui)
  *  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:
     <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 <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 <code>new-</code> before metdata keys that should be
     transferred to the new zettel)
  *  New suported metadata key <code>box-number</code>, which gives an
     indication from which box the zettel was loaded.
     (minor)
  *  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>place-<em>X</em>-uri</tt> (where <em>X</em> is a
     number greater than zero) has been renamed to
     <tt>place-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 place
     was a name collision with the &ldquo;simple&rdquo; directory box sub-type.
     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=place/constplace/emoji_spin.gif].
     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 inprovements, to the software and to the
  *  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 places. The original directory place type is now called "notify"
     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-place-type</tt>, which gives
     the default value for specifying a directory place 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 place schema "file" allows to read zettel from a ZIP file.
  *  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
  *  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.
     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 filtering a list of zettel, it can be specified that a given value should
     <em>not</em> match. Previously, only the whole filter expression could be
  *  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 filter a zettel list by specifying that specific metadata keys must
  *  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.
  *  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)
  *  Filtering 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.
  *  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 inprovements, 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 place must not contain more than approx. 250 files. There are three options
to mitigate this limitation temporarily:
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 virtualization 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)
  *  Searching/filtering ignores the leading '#' character of tags.
  *  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 filtering 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 inprovements, 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
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 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 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;.
  *  (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 places.
             Some other computed zettel got a new identifier (to make room for other variant).
  *  (minor) Remove zettel <tt>00000000000004</tt>, 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 Environment Values</i>).
  *  (minor) Predefined templates for new zettel do not contain any value for attribute <tt>visibility</tt> any more.
             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
             <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
             <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 <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 <code>00000000000006</code> (<i>Zettelstore
             Environment Values</i>).
  *  (minor) Predefined templates for new zettel do not contain any value for
             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.
             It is a non-empty string, that will be formatted with
             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;.
             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>.
  *  (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. <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.
  *  (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 <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.
             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 &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 place,
  *        its metadata are just calculated from the information available.
           Updated metadata did not find its way into the place, because the <tt>.meta</tt> file was not written.
           This has been fixed.
  *  (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.
  *  (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 <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 <code>.meta</code> file was not
           written.
  *  (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.
           This has been fixed.
           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) 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).
           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>.
  *  (bug) When renaming a zettel, check all places for the new zettel identifier, not just the first place.
           Otherwise it will be possible to shadow a read-only zettel from a next place, 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.
             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
             <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.
             An owner becomes an expert, if the runtime configuration key
             <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.
  *  (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.
             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 <code>readonly</code> of <i>Zettelstore Startup
             Configuration</i> to <code>read-only-mode</code>. This was done to
             avoid some confusion with the 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.
             <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.
  *  (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 <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) 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.
  *  (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 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.)
  *  (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 <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 (<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.
     [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>
  *  (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>
  *  (minor) Rename the scheme of the place URL where predefined zettel are stored to &ldquo;const&rdquo;.
             The previous value was &ldquo;globals&rdquo;.
  *  (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to
             <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
             <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 parameter of filtering a list 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 <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>).
  *  (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 <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.

<h3>Web user interface (WebUI)</h3>
  *  (minor) Focus on the first text field on some forms (new zettel, edit zettel, rename zettel, login)
  *  (minor) Focus on the first text field on some forms (new zettel, edit
             zettel, rename zettel, login)
  *  (major) Adapt all HTML templates to a simpler structure.
  *  (bug) Rendered wrong URLs for internal links on info page.
  *  (bug) If a zettel contains binary content it cannot be cloned.
           For such a zettel only the metadata can be changed.
  *  (minor) Non-zettel references that neither have an URL scheme, user info, 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 placed after an external URL to <tt>&\#8599;&\#xfe0e;</tt> (&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>.
  *  (bug)   Rendered wrong URLs for internal links on info page.
  *  (bug)   If a zettel contains binary content it cannot be cloned.
             For such a zettel only the metadata can be changed.
  *  (minor) Non-zettel references that neither have an URL scheme, user info,
             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 put after an
             external URL to <code>&\#8599;&\#xfe0e;</code>
             (&ldquo;&#8599;&#xfe0e;&rdquo;). This affects the former key
             <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 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>
             <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.
  *  (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.
             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 <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: <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
             <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 places, where zettel are stored, via an URL.
  *  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 zettel list.
     If you changed the configuration zettel, you should rename it manually in its file directory.
  *  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




1
2
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.0.13</code> (2021-06-01).
Build: <code>v0.17.0</code> (2024-03-04).

  *  [/uv/zettelstore-0.0.13-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.0.13-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.0.13-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.0.13-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.0.13-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/uv/zettelstore-0.17.0-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.17.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.17.0-darwin-arm64.zip|macOS] (arm64)
  *  [/uv/zettelstore-0.17.0-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.17.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.0.13.zip|here].
Just unzip the contained files and put them into your zettel folder or configure
a file place to read the zettel directly from the ZIP file.
As a starter, you can download the zettel for the manual
[/uv/manual-0.17.0.zip|here].
Just unzip the contained files and put them into your zettel folder or
configure a file box to read the zettel directly from the ZIP file.

Changes to www/index.wiki.

1
2
3
4
5
6
7
8
9
10





11
12
13



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

41
42
43
44
45
46





-
-
-
-
-
+
+
+
+
+

-
-
+
+
+




+
+
+
+
+
+
+
+
-
+

-
+

-
-
-
-
-
+
+
+
+
+





-
+





<title>Home</title>

<b>Zettelstore</b> is a software that collects and relates your notes
(&ldquo;zettel&rdquo;) to represent and enhance your knowledge. It helps with
many tasks of personal knowledge management by explicitly supporting the
[https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The
method is based on creating many individual notes, each with one idea or
information, that are related to each other. Since knowledge is typically build
up gradually, one major focus is a long-term store of these notes, hence the
name &ldquo;Zettelstore&rdquo;.
[https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The method is
based on creating many individual notes, each with one idea or information,
that are related to each other. Since knowledge is typically build up
gradually, one major focus is a long-term store of these notes, hence the name
&ldquo;Zettelstore&rdquo;.

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.
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://zettelstore.de/sx|Sx] provides an evaluator for symbolic
     expressions, which is unsed for HTML templates and more.

[https://twitter.com/zettelstore|Stay tuned]&hellip;
[https://mastodon.social/tags/Zettelstore|Stay tuned]&nbsp;&hellip;
<hr>
<h3>Latest Release: 0.0.13 (2021-06-01)</h3>
<h3>Latest Release: 0.17.0 (2024-03-04)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_0_13|Change summary]
  *  [/timeline?p=version-0.0.13&bt=version-0.0.12&y=ci|Check-ins for version 0.0.13],
     [/vdiff?to=version-0.0.13&from=version-0.0.12|content diff]
  *  [/timeline?df=version-0.0.13&y=ci|Check-ins derived from the 0.0.13 release],
     [/vdiff?from=version-0.0.13&to=trunk|content diff]
  *  [./changes.wiki#0_17|Change summary]
  *  [/timeline?p=v0.17.0&bt=v0.16.0&y=ci|Check-ins for version 0.17.0],
     [/vdiff?to=v0.17.0&from=v0.16.0|content diff]
  *  [/timeline?df=v0.17.0&y=ci|Check-ins derived from the 0.17.0 release],
     [/vdiff?from=v0.17.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
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





-
-
-
-
+
-
-
-
-
-
+
-


-
-





-
+
-
-
-
+
-
-
-
-
-
+
+
+

<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>
  *  Content with binary data (e.g. a GIF, PNG, or JPG file) cannot be created
     nor modified via the standard web interface. As a workaround, you should
     place your file into the directory where your zettel are stored. Make sure
  *  Zettelstore must have indexed all zettel to make use of queries.
     that the file name starts with unique 14 digits that make up the zettel
     identifier.
  *  Automatic lists and transclusions are not supported in Zettelmarkup.
  *  &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 <tt>file</tt> sub-command currently does not support output format
     &ldquo;json&rdquo;.
  *  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.
  *  Backspace character in links does not always work, esp. for <tt>\|</tt> or
  *  Some file systems differentiate filenames with different cases (e.g. some
     <tt>\]</tt>.
  *  &hellip;

     on Linux, sometimes on macOS), others do not (default on macOS, most on
<h3>Planned improvements</h3>
  *  Support for mathematical content is missing, e.g. <code>$$F(x) &=
     \\int^a_b \\frac{1}{3}x^3$$</code>.
  *  Render zettel in [https://pandoc.org|pandoc's] JSON version of
     their native AST to make pandoc an external renderer for Zettelstore.
     Windows). Zettelstore is not able to detect these differences. Do not put
     files in your directory boxes and in files boxes that differ only by upper
     / lower case letters.
  *  &hellip;

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"

	"zettelstore.de/client.fossil/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 !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
	}
	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 {
		for next := range closure {
			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.Substract(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 len(closure) == 0 {
			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)
		for next := range dg[curr] {
			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].Sorted()
	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)
		for next := range closure {
			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)
		for next := range closure {
			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 len(terms) == 0 {
			break
		}
		termSlice := terms.Sorted()
		slices.Reverse(termSlice)
		sl = append(sl, termSlice...)
		for t := range terms {
			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
167
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
)

// Zid is the internal identifier of a zettel. Typically, it is a
// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
// A zettelstore implementation should try to set the last two digits to zero,
// e.g. the seconds should be zero,
type Zid uint64

// 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)
	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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 (
	"maps"
	"strings"
)

// Set is a set of zettel identifier
type Set map[Zid]struct{}

// String returns a string representation of the map.
func (s Set) String() string {
	if s == nil {
		return "{}"
	}
	var sb strings.Builder
	sb.WriteByte('{')
	for i, zid := range s.Sorted() {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.Write(zid.Bytes())
	}
	sb.WriteByte('}')
	return sb.String()
}

// 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.CopySlice(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.CopySlice(zids)
	return result
}

// Clone returns a copy of the given set.
func (s Set) Clone() Set {
	if len(s) == 0 {
		return nil
	}
	return maps.Clone(s)
}

// Add adds a Add to the set.
func (s Set) Add(zid Zid) Set {
	if s == nil {
		return NewSet(zid)
	}
	s[zid] = struct{}{}
	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 {
	if s != nil {
		_, found := s[zid]
		return found
	}
	return false
}

// ContainsOrNil return true if the set is nil or if the set contains the given Zettel identifier.
func (s Set) ContainsOrNil(zid Zid) bool {
	if s != nil {
		_, found := s[zid]
		return found
	}
	return true
}

// Copy adds all member from the other set.
func (s Set) Copy(other Set) Set {
	if s == nil {
		if len(other) == 0 {
			return nil
		}
		s = NewSetCap(len(other))
	}
	maps.Copy(s, other)
	return s
}

// CopySlice adds all identifier of the given slice to the set.
func (s Set) CopySlice(sl Slice) Set {
	if s == nil {
		s = NewSetCap(len(sl))
	}
	for _, zid := range sl {
		s[zid] = struct{}{}
	}
	return s
}

// 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
}

// Substract removes all zettel identifier from 's' that are in the set 'other'.
func (s Set) Substract(other Set) {
	if s == nil || other == nil {
		return
	}
	for zid := range other {
		delete(s, zid)
	}
}

// Remove the identifier from the set.
func (s Set) Remove(zid Zid) Set {
	if len(s) == 0 {
		return nil
	}
	delete(s, zid)
	if len(s) == 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 { return maps.Equal(s, other) }

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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 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.ContainsOrNil(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.Copy(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.Substract(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 range b.N {
//			s[id.Zid(i)] = true
//		}
//	}
func BenchmarkSet(b *testing.B) {
	s := id.Set{}
	for i := range b.N {
		s[id.Zid(i)] = struct{}{}
	}
}

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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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) }

func (zs Slice) String() 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 TestSliceString(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  id.Slice
		exp string
	}{
		{nil, ""},
		{id.Slice{}, ""},
		{id.Slice{1}, "00000000000001"},
		{id.Slice{1, 2}, "00000000000001 00000000000002"},
	}
	for i, tc := range testcases {
		got := tc.in.String()
		if got != tc.exp {
			t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got)
		}
	}
}

Added 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 "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
}

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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/client.fossil/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
}

// 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.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(api.KeyDead, TypeIDSet, usageProperty, "")
	registerKey(api.KeyExpire, TypeTimestamp, usageUser, "")
	registerKey(api.KeyFolgeRole, TypeWord, usageUser, "")
	registerKey(api.KeyForward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyLang, TypeWord, usageUser, "")
	registerKey(api.KeyLicense, TypeEmpty, usageUser, "")
	registerKey(api.KeyModified, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge)
	registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors)
	registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "")
	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)
		}
	}
	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 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"

	"zettelstore.de/client.fossil/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
173
174
175
176
177
178
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/client.fossil/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 {
		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 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
}

// 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, 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, " ")
	}
}

// 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"

	"zettelstore.de/client.fossil/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"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const testID = id.Zid(98765432101234)

func newMeta(title string, tags []string, syntax string) *meta.Meta {
	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)
}