Zettelstore

Check-in Differences
Login

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

Difference From version-0.0.14 To version-0.0.13

2021-07-23
16:43
Increase version to 0.0.15-dev to begin next development cycle ... (check-in: ba65777850 user: stern tags: trunk)
11:02
Version 0.0.14 ... (check-in: 6fe53d5db2 user: stern tags: trunk, release, version-0.0.14)
2021-07-22
13:50
Add client API for retrieving zettel links ... (check-in: e43ff68174 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)

Changes to Makefile.

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1
2
3
4
5
6
7
8
9

10
11
12
13
14



15
16
17
18
19
20
21
22
23
24
25









-
+




-
-
-












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

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

check:
	go run tools/build.go check

api:
	go run tools/build.go testapi

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

build:
	go run tools/build.go build

release:
	go run tools/build.go release

clean:
	go run tools/build.go clean

Changes to VERSION.

1


1
-
+
0.0.14
0.0.13

Deleted 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
63
64
65
66
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 api contains common definition used for client and server.
package api

// AuthJSON contains the result of an authentication call.
type AuthJSON struct {
	Token   string `json:"token"`
	Type    string `json:"token_type"`
	Expires int    `json:"expires_in"`
}

// ZidJSON contains the identifier data of a zettel.
type ZidJSON struct {
	ID  string `json:"id"`
	URL string `json:"url"`
}

// ZidMetaJSON contains the identifier and the metadata of a zettel.
type ZidMetaJSON struct {
	ID   string            `json:"id"`
	URL  string            `json:"url"`
	Meta map[string]string `json:"meta"`
}

// ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel
type ZidMetaRelatedList struct {
	ID   string            `json:"id"`
	URL  string            `json:"url"`
	Meta map[string]string `json:"meta"`
	List []ZidMetaJSON     `json:"list"`
}

// ZettelLinksJSON store all links / connections from one zettel to other.
type ZettelLinksJSON struct {
	ID    string `json:"id"`
	URL   string `json:"url"`
	Links struct {
		Incoming []ZidJSON `json:"incoming,omitempty"`
		Outgoing []ZidJSON `json:"outgoing,omitempty"`
		Local    []string  `json:"local,omitempty"`
		External []string  `json:"external,omitempty"`
		Meta     []string  `json:"meta,omitempty"`
	} `json:"links"`
	Images struct {
		Outgoing []ZidJSON `json:"outgoing,omitempty"`
		Local    []string  `json:"local,omitempty"`
		External []string  `json:"external,omitempty"`
	} `json:"images,omitempty"`
	Cites []string `json:"cites,omitempty"`
}

// ZettelDataJSON contains all data for a zettel.
type ZettelDataJSON struct {
	Meta     map[string]string `json:"meta"`
	Encoding string            `json:"encoding"`
	Content  string            `json:"content"`
}

// ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content.
type ZettelJSON struct {
	ID       string            `json:"id"`
	URL      string            `json:"url"`
	Meta     map[string]string `json:"meta"`
	Encoding string            `json:"encoding"`
	Content  string            `json:"content"`
}

// ZettelListJSON contains data for a zettel list.
type ZettelListJSON struct {
	List []ZettelJSON `json:"list"`
}

// TagListJSON specifies the list/map of tags
type TagListJSON struct {
	Tags map[string][]string `json:"tags"`
}

// RoleListJSON specifies the list of roles.
type RoleListJSON struct {
	Roles []string `json:"role-list"`
}

Deleted api/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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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) 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 contains common definition used for client and server.
package api

import (
	"fmt"
)

// Additional HTTP constants used.
const (
	MethodMove = "MOVE" // HTTP method for renaming a zettel

	HeaderAccept      = "Accept"
	HeaderContentType = "Content-Type"
	HeaderDestination = "Destination"
	HeaderLocation    = "Location"
)

// Values for HTTP query parameter.
const (
	QueryKeyDepth  = "depth"
	QueryKeyDir    = "dir"
	QueryKeyFormat = "_format"
	QueryKeyLimit  = "limit"
	QueryKeyPart   = "_part"
)

// Supported dir values.
const (
	DirBackward = "backward"
	DirForward  = "forward"
)

// Supported format values.
const (
	FormatDJSON  = "djson"
	FormatHTML   = "html"
	FormatJSON   = "json"
	FormatNative = "native"
	FormatRaw    = "raw"
	FormatText   = "text"
	FormatZMK    = "zmk"
)

var formatEncoder = map[string]EncodingEnum{
	FormatDJSON:  EncoderDJSON,
	FormatHTML:   EncoderHTML,
	FormatJSON:   EncoderJSON,
	FormatNative: EncoderNative,
	FormatRaw:    EncoderRaw,
	FormatText:   EncoderText,
	FormatZMK:    EncoderZmk,
}
var encoderFormat = map[EncodingEnum]string{}

func init() {
	for k, v := range formatEncoder {
		encoderFormat[v] = k
	}
}

// Encoder returns the internal encoder code for the given format string.
func Encoder(format string) EncodingEnum {
	if e, ok := formatEncoder[format]; ok {
		return e
	}
	return EncoderUnknown
}

// EncodingEnum lists all valid encoder keys.
type EncodingEnum uint8

// Values for EncoderEnum
const (
	EncoderUnknown EncodingEnum = iota
	EncoderDJSON
	EncoderHTML
	EncoderJSON
	EncoderNative
	EncoderRaw
	EncoderText
	EncoderZmk
)

// String representation of an encoder key.
func (e EncodingEnum) String() string {
	if f, ok := encoderFormat[e]; ok {
		return f
	}
	return fmt.Sprintf("*Unknown*(%d)", e)
}

// Supported part values.
const (
	PartID      = "id"
	PartMeta    = "meta"
	PartContent = "content"
	PartZettel  = "zettel"
)

Deleted api/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
111
112
113
114


















































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 contains common definition used for client and server.
package api

import (
	"net/url"
	"strings"

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

type urlQuery struct{ key, val string }

// URLBuilder should be used to create zettelstore URLs.
type URLBuilder struct {
	prefix   string
	key      byte
	path     []string
	query    []urlQuery
	fragment string
}

// NewURLBuilder creates a new URL builder with the given prefix and key.
func NewURLBuilder(prefix string, key byte) *URLBuilder {
	return &URLBuilder{prefix: prefix, key: key}
}

// Clone an URLBuilder
func (ub *URLBuilder) Clone() *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) *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) *URLBuilder {
	ub.path = append(ub.path, p)
	return ub
}

// AppendQuery adds a new query parameter
func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder {
	ub.query = append(ub.query, urlQuery{key, value})
	return ub
}

// ClearQuery removes all query parameters.
func (ub *URLBuilder) ClearQuery() *URLBuilder {
	ub.query = nil
	ub.fragment = ""
	return ub
}

// SetFragment stores the fragment
func (ub *URLBuilder) SetFragment(s string) *URLBuilder {
	ub.fragment = s
	return ub
}

// String produces a string value.
func (ub *URLBuilder) String() string {
	var sb strings.Builder

	sb.WriteString(ub.prefix)
	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 ast/ast.go.

28
29
30
31
32
33
34
35

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

35
36
37
38
39
40
41
42







-
+







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

// Node is the interface, all nodes must implement.
type Node interface {
	WalkChildren(v Visitor)
	Accept(v Visitor)
}

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

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

2
3
4
5
6
7
8
9
10
11
12
13
14
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
// 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) {
	t.Parallel()
	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) {
	t.Parallel()
	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"}}

Changes to ast/block.go.

15
16
17
18
19
20
21
22
23
24



25
26
27


28
29
30
31
32
33
34
35

36
37
38
39
40
41


42
43
44
45

46
47
48
49
50
51
52


53
54
55


56
57
58
59
60
61

62
63
64
65
66
67
68


69
70
71
72

73
74
75
76
77
78
79


80
81
82


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98


99
100
101


102
103
104
105
106
107
108
109
110
111
112
113


114
115
116


117
118
119
120
121
122

123
124
125
126
127
128


129
130
131
132

133
134
135
136
137
138
139


140
141
142


143
144
145
146
147
148
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
15
16
17
18
19
20
21



22
23
24
25


26
27


28
29
30
31
32

33
34
35
36
37


38
39
40
41
42

43
44
45
46
47
48


49
50
51


52
53
54
55
56
57
58

59
60
61
62
63
64


65
66
67
68
69

70
71
72
73
74
75


76
77
78


79
80



81
82
83
84
85
86
87
88
89
90
91


92
93
94


95
96


97
98
99
100
101
102
103
104


105
106
107


108
109
110
111
112
113
114

115
116
117
118
119


120
121
122
123
124

125
126
127
128
129
130


131
132
133


134
135




136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151


152
153







154
155
156
157
158
159
160







-
-
-
+
+
+

-
-
+
+
-
-





-
+




-
-
+
+



-
+





-
-
+
+

-
-
+
+





-
+





-
-
+
+



-
+





-
-
+
+

-
-
+
+
-
-
-











-
-
+
+

-
-
+
+
-
-








-
-
+
+

-
-
+
+





-
+




-
-
+
+



-
+





-
-
+
+

-
-
+
+
-
-
-
-
















-
-
+
+
-
-
-
-
-
-
-








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

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

// WalkChildren walks down the inline elements.
func (pn *ParaNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (pn *ParaNode) Accept(v Visitor) { v.VisitPara(pn) }
	WalkInlineSlice(v, pn.Inlines)
}

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

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

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

// Constants for VerbatimCode
const (
	_               VerbatimKind = iota
	_               VerbatimCode = iota
	VerbatimProg                 // Program code.
	VerbatimComment              // Block comment
	VerbatimHTML                 // Block HTML, e.g. for Markdown
)

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

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

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

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

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

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

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

// WalkChildren walks down the blocks and the text.
func (rn *RegionNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (rn *RegionNode) Accept(v Visitor) { v.VisitRegion(rn) }
	WalkBlockSlice(v, rn.Blocks)
	WalkInlineSlice(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
	Attrs   *Attributes
}

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

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

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

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

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

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

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

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

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

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

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

// WalkChildren walks down the items.
func (ln *NestedListNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (ln *NestedListNode) Accept(v Visitor) { v.VisitNestedList(ln) }
	for _, item := range ln.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() {}

// WalkChildren walks down to the descriptions.
func (dn *DescriptionListNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (dn *DescriptionListNode) Accept(v Visitor) { v.VisitDescriptionList(dn) }
	for _, desc := range dn.Descriptions {
		WalkInlineSlice(v, desc.Term)
		for _, dns := range desc.Descriptions {
			WalkDescriptionSlice(v, dns)
		}
	}
}

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

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


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







-
+

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











-
+

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

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

// WalkChildren walks down to the cells.
func (tn *TableNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (tn *TableNode) Accept(v Visitor) { v.VisitTable(tn) }
	for _, cell := range tn.Header {
		WalkInlineSlice(v, cell.Inlines)
	}
	for _, row := range tn.Rows {
		for _, cell := range row {
			WalkInlineSlice(v, cell.Inlines)
		}
	}
}

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

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

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

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

Changes to ast/inline.go.

14
15
16
17
18
19
20
21

22
23
24


25
26
27
28
29
30
31
32
33

34
35
36


37
38
39
40
41
42
43
44
45

46
47
48


49
50
51
52
53
54
55
56
57

58
59
60


61
62
63
64
65
66
67
68
69
70
71
72

73
74
75


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

91
92
93


94
95
96
97
98
99
100
101
102
103
104
105
106

107
108
109


110
111
112
113
114
115
116
117
118
119
120
121
122

123
124
125


126
127
128
129
130
131
132
133
134
135

136
137
138


139
140
141
142
143
144
145
146

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


14
15
16
17
18
19
20

21
22


23
24
25
26
27
28
29
30
31
32

33
34


35
36
37
38
39
40
41
42
43
44

45
46


47
48
49
50
51
52
53
54
55
56

57
58


59
60
61
62
63
64
65
66
67
68
69
70
71

72
73


74
75


76
77
78
79
80
81
82
83
84
85
86
87

88
89


90
91


92
93
94
95
96
97
98
99
100
101

102
103


104
105


106
107
108
109
110
111
112
113
114
115

116
117


118
119
120
121
122
123
124
125
126
127
128

129
130


131
132


133
134
135
136
137

138
139
140
141
142


143
144
145
146
147

148
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







-
+

-
-
+
+








-
+

-
-
+
+








-
+

-
-
+
+








-
+

-
-
+
+











-
+

-
-
+
+
-
-












-
+

-
-
+
+
-
-










-
+

-
-
+
+
-
-










-
+

-
-
+
+









-
+

-
-
+
+
-
-





-
+




-
-
+
+



-
+

















-
+

-
-
+
+
-
-





-
+




-
-
+
+



-
+







-
+

-
-
+
+
// Definitions of inline nodes.

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

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

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

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

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

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

// WalkChildren does nothing.
func (tn *TagNode) WalkChildren(v Visitor) { /* No children*/ }
// 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() { /* Just a marker */ }
func (sn *SpaceNode) inlineNode() {}

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

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

// 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() { /* Just a marker */ }
func (bn *BreakNode) inlineNode() {}

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

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

// LinkNode contains the specified link.
type LinkNode struct {
	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() { /* Just a marker */ }
func (ln *LinkNode) inlineNode() {}

// WalkChildren walks to the link text.
func (ln *LinkNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (ln *LinkNode) Accept(v Visitor) { v.VisitLink(ln) }
	WalkInlineSlice(v, ln.Inlines)
}

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

// ImageNode contains the specified image reference.
type ImageNode struct {
	Ref     *Reference  // Reference to image
	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.
	Attrs   *Attributes // Optional attributes
}

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

// WalkChildren walks to the image text.
func (in *ImageNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (in *ImageNode) Accept(v Visitor) { v.VisitImage(in) }
	WalkInlineSlice(v, in.Inlines)
}

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

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

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

// WalkChildren walks to the cite text.
func (cn *CiteNode) WalkChildren(v Visitor) {
// Accept a visitor and visit the node.
func (cn *CiteNode) Accept(v Visitor) { v.VisitCite(cn) }
	WalkInlineSlice(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 {
	Text string
}

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

// WalkChildren does nothing.
func (mn *MarkNode) WalkChildren(v Visitor) { /* No children*/ }
// Accept a visitor and visit the node.
func (mn *MarkNode) Accept(v Visitor) { v.VisitMark(mn) }

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

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

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

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

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

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

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

// Constants for FormatCode
const (
	_               FormatKind = iota
	_               FormatCode = iota
	FormatItalic               // Italic text.
	FormatEmph                 // Semantically emphasized text.
	FormatBold                 // Bold text.
	FormatStrong               // Semantically strongly emphasized text.
	FormatUnder                // Underlined 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.
	FormatQuotation            // Quotation text.
	FormatSmall                // Smaller text.
	FormatSpan                 // Generic inline container.
	FormatMonospace            // Monospaced text.
)

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

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

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

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

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

// Constants for LiteralCode
const (
	_              LiteralKind = iota
	_              LiteralCode = iota
	LiteralProg                // Inline program code.
	LiteralKeyb                // Keyboard strokes.
	LiteralOutput              // Sample output.
	LiteralComment             // Inline comment
	LiteralHTML                // Inline HTML, e.g. for Markdown
)

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

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

Changes to ast/ref_test.go.

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

21
22
23
24
25
26
27







-







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"},
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
37
38
39
40
41
42
43

44
45
46
47
48
49
50







-







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

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

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

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






















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 ast provides the abstract syntax tree.
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
	}
	node.WalkChildren(v)
	v.Visit(nil)
}

// WalkBlockSlice traverse a block slice.
func WalkBlockSlice(v Visitor, bns BlockSlice) {
	for _, bn := range bns {
		Walk(v, bn)
	}
}

// WalkInlineSlice traverses an inline slice.
func WalkInlineSlice(v Visitor, ins InlineSlice) {
	for _, in := range ins {
		Walk(v, in)
	}
}

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

Changes to auth/auth.go.

10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27







-



+








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

import (
	"time"

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

// 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
75
76
77
78
79
80
81
82

83
84
85
86
87
88
89
75
76
77
78
79
80
81

82
83
84
85
86
87
88
89







-
+







}

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

	BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy)
	PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, 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

Changes to auth/impl/impl.go.

17
18
19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
35







-




+







	"io"
	"time"

	"github.com/pascaldekloe/jwt"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
	"zettelstore.de/z/web/server"
)

type myAuth struct {
	readonly bool
	owner    id.Zid
	secret   []byte
175
176
177
178
179
180
181
182
183


184
175
176
177
178
179
180
181


182
183
184







-
-
+
+

		if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown {
			return ur
		}
	}
	return meta.UserRoleReader
}

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

Deleted 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




































































































































































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

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

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

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

// newBox creates a new policy box.
func newBox(auth server.Auth, box box.Box, policy auth.Policy) box.Box {
	return &polBox{
		auth:   auth,
		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 domain.Zettel) (id.Zid, error) {
	user := pp.auth.GetUser(ctx)
	if pp.policy.CanCreate(user, zettel.Meta) {
		return pp.box.CreateZettel(ctx, zettel)
	}
	return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid)
}

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

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

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

func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
	return pp.box.GetAllMeta(ctx, zid)
}

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

func (pp *polBox) 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.box.SelectMeta(ctx, s)
}

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

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

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

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

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

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

Changes to auth/policy/owner.go.

60
61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77
78
79
80
81
60
61
62
63
64
65
66



67





68
69
70
71
72
73
74







-
-
-
+
-
-
-
-
-







	if user == nil {
		return false
	}
	if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
		// 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,
101
102
103
104
105
106
107
108

109
110
111
112
113
114
115
116
94
95
96
97
98
99
100

101

102
103
104
105
106
107
108







-
+
-







		for _, key := range noChangeUser {
			if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") {
				return false
			}
		}
		return true
	}
	switch userRole := o.manager.GetUserRole(user); userRole {
	if o.manager.GetUserRole(user) == meta.UserRoleReader {
	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) {

Added 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_test.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
17
18
19
20
21
22
23

24
25
26
27
28
29
30







-








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

func TestPolicies(t *testing.T) {
	t.Parallel()
	testScene := []struct {
		readonly bool
		withAuth bool
		expert   bool
	}{
		{true, true, true},
		{true, true, false},
85
86
87
88
89
90
91




92




93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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
84
85
86
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112
113
114
115
116
117
118

119
120
121
122
123
124

125
126
127
128
129
130

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148

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







+
+
+
+
-
+
+
+
+







-













-






-






-


















-






-











-






-






-




-
-
-
-
-
-
-


-






-






-






-





-




















-








type authConfig struct{ expert bool }

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 {
		switch vis {
		case meta.ValueVisibilityPublic:
			return meta.VisibilityPublic
		case meta.ValueVisibilityOwner:
		return meta.GetVisibility(vis)
			return meta.VisibilityOwner
		case meta.ValueVisibilityExpert:
			return meta.VisibilityExpert
		}
	}
	return meta.VisibilityLogin
}

func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly, isExpert 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) {
	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()
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
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







-






-






-






-






-






-






-






-






-






-





-








-






-






-






-






-


















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



















-






-






-






-






-






-






-






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
















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



-
-
-
-
-
-
-







		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()
	reader := newReader()
	writer := newWriter()
	creator := newCreator()
	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},
		{reader, nil, false},
		{writer, nil, false},
		{owner, nil, false},
		{owner2, nil, false},
		// Any zettel
		{anonUser, zettel, notAuthNotReadonly},
		{reader, zettel, notAuthNotReadonly},
		{writer, zettel, notAuthNotReadonly},
		{owner, zettel, !readonly},
		{owner2, zettel, !readonly},
		// Expert zettel
		{anonUser, 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},
		{reader, roFalse, notAuthNotReadonly},
		{writer, roFalse, notAuthNotReadonly},
		{owner, roFalse, !readonly},
		{owner2, roFalse, !readonly},
		// Reader r/o zettel
		{anonUser, roReader, false},
		{reader, roReader, false},
		{writer, roReader, notAuthNotReadonly},
		{owner, roReader, !readonly},
		{owner2, roReader, !readonly},
		// Writer r/o zettel
		{anonUser, roWriter, false},
		{reader, roWriter, false},
		{writer, roWriter, false},
		{owner, roWriter, !readonly},
		{owner2, roWriter, !readonly},
		// Owner r/o zettel
		{anonUser, roOwner, false},
		{reader, roOwner, false},
		{writer, roOwner, false},
		{owner, roOwner, false},
		{owner2, roOwner, false},
		// r/o = true zettel
		{anonUser, 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()
	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)
			}
		})
	}
}

const (
	creatorZid = id.Zid(1013)
	readerZid  = id.Zid(1013)
	writerZid  = id.Zid(1015)
	readerZid = id.Zid(1013)
	writerZid = id.Zid(1015)
	ownerZid  = id.Zid(1017)
	ownerZid   = id.Zid(1017)
	owner2Zid  = id.Zid(1019)
	zettelZid  = id.Zid(1021)
	visZid     = id.Zid(1023)
	userZid    = id.Zid(1025)
	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(meta.KeyTitle, "Creator")
	user.Set(meta.KeyRole, meta.ValueRoleUser)
	user.Set(meta.KeyUserRole, meta.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)
	return user
}
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
560
561
562
563
564
565
566






567
568
569
570
571
572
573







-
-
-
-
-
-







}
func newPublicZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Public Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
	return m
}
func newCreatorZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Creator Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator)
	return m
}
func newLoginZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Login Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin)
	return m
}
func newOwnerZettel() *meta.Meta {

Deleted 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


















































































































































































































































































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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	// 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 box statistics
	ReadStats(st *ManagedBoxStats)
}

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

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

// StartStopper performs simple lifecycle management.
type StartStopper interface {
	// Start the box. Now all other functions of the box are allowed.
	// Starting an already started box is not allowed.
	Start(ctx context.Context) error

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

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

	// SelectMeta returns a list of metadata that comply to the given selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)

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

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

// 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
	OnReload              // Box 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 {
	Box    Box
	Reason UpdateReason
	Zid    id.Zid
}

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

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

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

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

// ErrNotFound is returned if a zettel was not found in the box.
var ErrNotFound = errors.New("zettel not found")

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

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

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

Deleted 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







































































































































































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

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

import (
	"context"
	"net/url"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
)

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 {
	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) string
}{
	id.VersionZid:              {genVersionBuildM, genVersionBuildC},
	id.HostZid:                 {genVersionHostM, genVersionHostC},
	id.OperatingSystemZid:      {genVersionOSM, genVersionOSC},
	id.BoxManagerZid:           {genManagerM, genManagerC},
	id.MetadataKeyZid:          {genKeysM, genKeysC},
	id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC},
}

// Get returns the one program box.
func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox {
	return &compBox{number: boxNumber, enricher: mf}
}

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

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

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

func (pp *compBox) CreateZettel(
	ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	return id.Invalid, box.ErrReadOnly
}

func (pp *compBox) 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{}, box.ErrNotFound
}

func (pp *compBox) 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, box.ErrNotFound
}

func (pp *compBox) 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 *compBox) 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.enricher.Enrich(ctx, m, pp.number)
				if match(m) {
					res = append(res, m)
				}
			}
		}
	}
	return res, nil
}

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

func (pp *compBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	return box.ErrReadOnly
}

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

func (pp *compBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if _, ok := myZettel[curZid]; ok {
		return box.ErrReadOnly
	}
	return box.ErrNotFound
}

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

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

func (pp *compBox) ReadStats(st *box.ManagedBoxStats) {
	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 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




















































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

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

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






































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

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

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








































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

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

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 Box Manager")
	return m
}

func genManagerC(*meta.Meta) string {
	kvl := kernel.Main.GetServiceStatistics(kernel.BoxService)
	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 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






















































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

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

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

Deleted 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
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 box/constbox/base.mustache.

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


































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<!DOCTYPE html>
<html{{#Lang}} lang="{{Lang}}"{{/Lang}}>
<head>
<meta charset="utf-8">
<meta name="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="{{{CSSBaseURL}}}">
<link rel="stylesheet" href="{{{CSSUserURL}}}">
<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>
{{#HasNewZettelLinks}}
<div class="zs-dropdown">
<button>New</button>
<nav class="zs-dropdown-content">
{{#NewZettelLinks}}
<a href="{{{URL}}}">{{Text}}</a>
{{/NewZettelLinks}}
</nav>
</div>
{{/HasNewZettelLinks}}
<form action="{{{SearchURL}}}">
<input type="text" placeholder="Search.." name="s">
</form>
</nav>
<main class="content">
{{{Content}}}
</main>
{{#FooterHTML}}
<footer>
{{{FooterHTML}}}
</footer>
{{/FooterHTML}}
</body>
</html>

Deleted 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





















































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 constbox puts zettel inside the executable.
package constbox

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

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
)

func init() {
	manager.Register(
		" const",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &constBox{
				number:   cdata.Number,
				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 constBox struct {
	number   int
	zettel   map[id.Zid]constZettel
	enricher box.Enricher
}

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

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

func (cp *constBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	return id.Invalid, box.ErrReadOnly
}

func (cp *constBox) 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{}, box.ErrNotFound
}

func (cp *constBox) 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, box.ErrNotFound
}

func (cp *constBox) 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 *constBox) 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, cp.number)
		if match(m) {
			res = append(res, m)
		}
	}
	return res, nil
}

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

func (cp *constBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	return box.ErrReadOnly
}

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

func (cp *constBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if _, ok := cp.zettel[curZid]; ok {
		return box.ErrReadOnly
	}
	return box.ErrNotFound
}
func (cp *constBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false }

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

func (cp *constBox) ReadStats(st *box.ManagedBoxStats) {
	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.KeySyntax:     meta.ValueSyntaxNone,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityOwner,
		},
		domain.NewContent("")},
	id.LicenseZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore License",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxText,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
		},
		domain.NewContent(contentLicense)},
	id.AuthorsZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Contributors",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
		},
		domain.NewContent(contentContributors)},
	id.DependenciesZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Dependencies",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
		},
		domain.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentBaseMustache)},
	id.LoginTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentLoginMustache)},
	id.ZettelTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentZettelMustache)},
	id.InfoTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentInfoMustache)},
	id.ContextTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Context HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentContextMustache)},
	id.FormTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentFormMustache)},
	id.RenameTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Rename Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentRenameMustache)},
	id.DeleteTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentDeleteMustache)},
	id.ListTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentListZettelMustache)},
	id.RolesTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Roles HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentListRolesMustache)},
	id.TagsTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Tags HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentListTagsMustache)},
	id.ErrorTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Error HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     syntaxTemplate,
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		domain.NewContent(contentErrorMustache)},
	id.BaseCSSZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     "css",
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
		},
		domain.NewContent(contentBaseCSS)},
	id.UserCSSZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore User CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     "css",
			meta.KeyVisibility: meta.ValueVisibilityPublic,
		},
		domain.NewContent("/* User-defined CSS */")},
	id.EmojiZid: {
		constHeader{
			meta.KeyTitle:      "Generic Emoji",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxGif,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
		},
		domain.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {
		constHeader{
			meta.KeyTitle:      "New Menu",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyVisibility: meta.ValueVisibilityCreator,
		},
		domain.NewContent(contentNewTOCZettel)},
	id.TemplateNewZettelZid: {
		constHeader{
			meta.KeyTitle:      "New Zettel",
			meta.KeyRole:       meta.ValueRoleZettel,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyVisibility: meta.ValueVisibilityCreator,
		},
		domain.NewContent("")},
	id.TemplateNewUserZid: {
		constHeader{
			meta.KeyTitle:                       "New User",
			meta.KeyRole:                        meta.ValueRoleUser,
			meta.KeySyntax:                      meta.ValueSyntaxNone,
			meta.NewPrefix + meta.KeyCredential: "",
			meta.NewPrefix + meta.KeyUserID:     "",
			meta.NewPrefix + meta.KeyUserRole:   meta.ValueUserRoleReader,
			meta.KeyVisibility:                  meta.ValueVisibilityOwner,
		},
		domain.NewContent("")},
	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 box/constbox/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 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

Deleted box/constbox/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 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
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 box/constbox/emoji_spin.gif.

cannot compute difference between binary files

Deleted box/constbox/error.mustache.

1
2
3
4
5
6






-
-
-
-
-
-
<article>
<header>
<h1>{{ErrorTitle}}</h1>
</header>
{{ErrorText}}
</article>

Deleted box/constbox/form.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38






































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<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 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
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 box/constbox/info.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<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>
{{#HasShadowLinks}}
<h2>Shadowed Boxes</h2>
<ul>{{#ShadowLinks}}<li>{{.}}</li>{{/ShadowLinks}}</ul>
{{/HasShadowLinks}}
{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}}
</article>

Deleted 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-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 box/constbox/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 box/constbox/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 box/constbox/listzettel.mustache.

1
2
3
4
5
6






-
-
-
-
-
-
<header>
<h1>{{Title}}</h1>
</header>
<ul>
{{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li>
{{/Metas}}</ul>

Deleted box/constbox/login.mustache.

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



















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<article>
<header>
<h1>{{Title}}</h1>
</header>
{{#Retry}}
<div class="zs-indication zs-error">Wrong user name / password. Try again.</div>
{{/Retry}}
<form method="POST" action="?_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 box/constbox/newtoc.zettel.

1
2
3
4




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

Deleted box/constbox/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 box/constbox/zettel.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28




























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<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 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
409
410
411
412
413
414
415
416
417
418
419
420




































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 dirbox provides a directory-based zettel box.
package dirbox

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

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/dirbox/directory"
	"zettelstore.de/z/box/filebox"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
)

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, 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 := dirBox{
			number:     cdata.Number,
			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
}

// dirBox uses a directory to store zettel as files.
type dirBox struct {
	number     int
	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 *dirBox) Location() string {
	return dp.location
}

func (dp *dirBox) 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 *dirBox) 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 *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) {
	if dp.mustNotify {
		if chci := dp.cdata.Notify; chci != nil {
			chci <- box.UpdateInfo{Reason: reason, Zid: zid}
		}
	}
}

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

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

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

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

	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(box.OnUpdate, meta.Zid)
	return meta.Zid, err
}

func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	entry, err := dp.dirSrv.GetEntry(zid)
	if err != nil || !entry.IsValid() {
		return domain.Zettel{}, box.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 *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	entry, err := dp.dirSrv.GetEntry(zid)
	if err != nil || !entry.IsValid() {
		return nil, box.ErrNotFound
	}
	m, err := getMeta(dp, entry, zid)
	if err != nil {
		return nil, err
	}
	dp.cleanupMeta(ctx, m)
	return m, nil
}

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

		if match(m) {
			res = append(res, m)
		}
	}
	if err != nil {
		return nil, err
	}
	return res, nil
}

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

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

	meta := zettel.Meta
	if !meta.Zid.IsValid() {
		return &box.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 box.
		entry = &directory.Entry{Zid: meta.Zid}
		dp.updateEntryFromMeta(entry, meta)
	} else if entry.MetaSpec == directory.MetaSpecNone {
		defaultMeta := filebox.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(box.OnUpdate, meta.Zid)
	}
	return err
}

func (dp *dirBox) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) {
	entry.MetaSpec, entry.ContentExt = dp.calcSpecExt(meta)
	basePath := dp.calcBasePath(entry)
	if entry.MetaSpec == directory.MetaSpecFile {
		entry.MetaPath = basePath + ".meta"
	}
	entry.ContentPath = basePath + "." + entry.ContentExt
	entry.Duplicates = false
}

func (dp *dirBox) calcBasePath(entry *directory.Entry) string {
	p := entry.ContentPath
	if p == "" {
		return filepath.Join(dp.dir, entry.Zid.String())
	}
	// ContentPath w/o the file extension
	return p[0 : len(p)-len(filepath.Ext(p))]
}

func (dp *dirBox) 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 *dirBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	return !dp.readonly
}

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

	// Check whether zettel with new ID already exists in this box.
	if _, err = dp.GetMeta(ctx, newZid); err == nil {
		return &box.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(box.OnDelete, curZid)
		dp.notifyChanged(box.OnUpdate, newZid)
	}
	return err
}

func (dp *dirBox) 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 *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if dp.readonly {
		return box.ErrReadOnly
	}

	entry, err := dp.dirSrv.GetEntry(zid)
	if err != nil || !entry.IsValid() {
		return box.ErrNotFound
	}
	dp.dirSrv.DeleteEntry(zid)
	err = deleteZettel(dp, entry, zid)
	if err == nil {
		dp.notifyChanged(box.OnDelete, zid)
	}
	return err
}

func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = dp.readonly
	st.Zettel, _ = dp.dirSrv.NumEntries()
}

func (dp *dirBox) 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 box/dirbox/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 box/dirbox/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 dirbox provides a directory-based zettel box.
package dirbox

import (
	"zettelstore.de/z/box/dirbox/notifydir"
	"zettelstore.de/z/box/dirbox/simpledir"
	"zettelstore.de/z/kernel"
)

func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) {
	for count := 0; count < 2; count++ {
		switch dirType {
		case kernel.BoxDirTypeNotify:
			return dirSrvNotify, 7, 1499
		case kernel.BoxDirTypeSimple:
			return dirSrvSimple, 1, 1
		default:
			dirType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string)
		}
	}
	panic("unable to set default dir box type: " + dirType)
}

func (dp *dirBox) 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 box/dirbox/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/box"
	"zettelstore.de/z/box/dirbox/directory"
	"zettelstore.de/z/domain/id"
)

// notifyService specifies a directory scan service.
type notifyService struct {
	dirPath    string
	rescanTime time.Duration
	done       chan struct{}
	cmds       chan dirCmd
	infos      chan<- box.UpdateInfo
}

// NewService creates a new directory service.
func NewService(directoryPath string, rescanTime time.Duration, chci chan<- box.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 box.UpdateReason, zid id.Zid) {
	if chci := srv.infos; chci != nil {
		chci <- box.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 box/dirbox/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/box"
	"zettelstore.de/z/box/dirbox/directory"
	"zettelstore.de/z/domain/id"
)

// 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(box.OnReload, id.Invalid)
			case fileStatusError:
				log.Println("DIRBOX", "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(box.OnUpdate, ev.zid)
	}
}

func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) {
	if newMap != nil {
		deleteFromMap(newMap, ev)
	} else {
		deleteFromMap(curMap, ev)
		srv.notifyChange(box.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 := box.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 <- &box.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 box/dirbox/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 box/dirbox/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
56
























































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 "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) {
	t.Parallel()
	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 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


































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 dirbox provides a directory-based zettel box.
package dirbox

import (
	"os"

	"zettelstore.de/z/box/dirbox/directory"
	"zettelstore.de/z/box/filebox"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
)

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 *dirBox, 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 = filebox.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 *dirBox, 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 = filebox.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 *dirBox, 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 *dirBox, 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) {
	filebox.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, 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 box/dirbox/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/box"
	"zettelstore.de/z/box/dirbox/directory"
	"zettelstore.de/z/domain/id"
)

// 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 := box.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 {
	// Nothing to to, since the actual file update is done by dirbox.
	return nil
}

func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error {
	// Nothing to to, since the actual file rename is done by dirbox.
	return nil
}

func (ss *simpleService) DeleteEntry(zid id.Zid) error {
	// Nothing to to, since the actual file delete is done by dirbox.
	return nil
}

Deleted 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






























































































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

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

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/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{
			number:   cdata.Number,
			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 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
249
250
251
252
253
254
255
256
257
258
259
260
261





































































































































































































































































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

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

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"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 zipBox struct {
	number   int
	name     string
	enricher box.Enricher
	zettel   map[id.Zid]*zipEntry // no lock needed, because read-only after creation
}

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

func (zp *zipBox) 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 *zipBox) 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 *zipBox) Stop(ctx context.Context) error {
	zp.zettel = nil
	return nil
}

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

func (zp *zipBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	return id.Invalid, box.ErrReadOnly
}

func (zp *zipBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	entry, ok := zp.zettel[zid]
	if !ok {
		return domain.Zettel{}, box.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 *zipBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	entry, ok := zp.zettel[zid]
	if !ok {
		return nil, box.ErrNotFound
	}
	reader, err := zip.OpenReader(zp.name)
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	return readZipMeta(reader, zid, entry)
}

func (zp *zipBox) 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 *zipBox) 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, zp.number)
		if match(m) {
			res = append(res, m)
		}
	}
	return res, nil
}

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

func (zp *zipBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	return box.ErrReadOnly
}

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

func (zp *zipBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if _, ok := zp.zettel[curZid]; ok {
		return box.ErrReadOnly
	}
	return box.ErrNotFound
}

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

func (zp *zipBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := zp.zettel[zid]; ok {
		return box.ErrReadOnly
	}
	return box.ErrNotFound
}

func (zp *zipBox) ReadStats(st *box.ManagedBoxStats) {
	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 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





































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

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

import (
	"time"

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

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

Deleted box/manager/anteroom.go.

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





































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 boxes 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 {
	num     uint64
	next    *anteroom
	waiting map[id.Zid]arAction
	curLoad int
	reload  bool
}

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

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

func (ar *anterooms) 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 put 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
	ar.nextNum++
	return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false}
}

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

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

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

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

func createWaitingSet(zids id.Set, 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, uint64) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		return arNothing, id.Invalid, 0
	}
	for zid, action := range ar.first.waiting {
		roomNo := ar.first.num
		delete(ar.first.waiting, zid)
		if len(ar.first.waiting) == 0 {
			ar.first = ar.first.next
			if ar.first == nil {
				ar.last = nil
			}
		}
		return action, zid, roomNo
	}
	return arNothing, id.Invalid, 0
}

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

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

import (
	"testing"

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

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

Deleted 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





















































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 boxes and indexes of a Zettelstore.
package manager

import (
	"context"
	"errors"
	"sort"
	"strings"

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

// Conatains all box.Box related functions

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

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

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

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return domain.Zettel{}, box.ErrStopped
	}
	for i, p := range mgr.boxes {
		if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, z.Meta, i+1)
			}
			return z, err
		}
	}
	return domain.Zettel{}, box.ErrNotFound
}

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

// GetMeta retrieves just the meta data of a specific zettel.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}
	for i, p := range mgr.boxes {
		if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, m, i+1)
			}
			return m, err
		}
	}
	return nil, box.ErrNotFound
}

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

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}
	for _, p := range mgr.boxes {
		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, box.ErrStopped
	}
	var result []*meta.Meta
	match := s.CompileMatch(mgr)
	for _, p := range mgr.boxes {
		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 = box.MergeSorted(result, selected)
		}
	}
	if s == nil {
		return result, nil
	}
	return s.Sort(result), nil
}

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

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

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

// RenameZettel changes the current zid to a new zid.
func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return box.ErrStopped
	}
	for i, p := range mgr.boxes {
		err := p.RenameZettel(ctx, curZid, newZid)
		if err != nil && !errors.Is(err, box.ErrNotFound) {
			for j := 0; j < i; j++ {
				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}
	return nil
}

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

// DeleteZettel removes the zettel from the box.
func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return box.ErrStopped
	}
	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zid)
		if err == nil {
			return nil
		}
		if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrNotFound
}

Deleted 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


















































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 boxes and indexes of a Zettelstore.
package manager

import (
	"strings"

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

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

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

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

func collectInlineIndexData(ins ast.InlineSlice, data *collectData) {
	ast.WalkInlineSlice(data, ins)
}

func (data *collectData) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.VerbatimNode:
		for _, line := range n.Lines {
			data.addText(line)
		}
	case *ast.TextNode:
		data.addText(n.Text)
	case *ast.TagNode:
		data.addText(n.Tag)
	case *ast.LinkNode:
		data.addRef(n.Ref)
	case *ast.ImageNode:
		data.addRef(n.Ref)
	case *ast.LiteralNode:
		data.addText(n.Text)
	}
	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[zid] = true
	}
}

Deleted 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




















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 boxes and indexes of a Zettelstore.
package manager

import (
	"context"
	"strconv"

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

// Enrich computes additional properties and updates the given metadata.
func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) {
	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 meta data
		return
	}
	m.Set(meta.KeyBoxNumber, strconv.Itoa(boxNumber))
	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 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



































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 boxes and indexes of a Zettelstore.
package manager

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

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

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchEqual(word string) id.Set {
	return mgr.idxStore.SearchEqual(word)
}

// 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 {
	return mgr.idxStore.SearchPrefix(prefix)
}

// 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 {
	return mgr.idxStore.SearchSuffix(suffix)
}

// 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 {
	return mgr.idxStore.SearchContains(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 := box.NoEnrichContext(context.Background())
	for {
		mgr.idxWorkService(ctx)
		if !mgr.idxSleepService(timer, timerDuration) {
			return
		}
	}
}

func (mgr *Manager) idxWorkService(ctx context.Context) {
	var roomNum uint64
	var start time.Time
	for {
		switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action {
		case arNothing:
			return
		case arReload:
			roomNum = 0
			zids, err := mgr.FetchZids(ctx)
			if err == nil {
				start = time.Now()
				if rno := mgr.idxAr.Reload(zids); rno > 0 {
					roomNum = rno
				}
				mgr.idxMx.Lock()
				mgr.idxLastReload = time.Now()
				mgr.idxSinceReload = 0
				mgr.idxMx.Unlock()
			}
		case arUpdate:
			zettel, err := mgr.GetZettel(ctx, zid)
			if err != nil {
				// TODO: on some errors put the zid into a "try later" set
				continue
			}
			mgr.idxMx.Lock()
			if arRoomNum == roomNum {
				mgr.idxDurReload = time.Since(start)
			}
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()
			mgr.idxUpdateZettel(ctx, zettel)
		case arDelete:
			if _, err := mgr.GetMeta(ctx, zid); err == nil {
				// Zettel was not deleted. This might occur, if zettel was
				// deleted in secondary dirbox, but is still present in
				// first dirbox (or vice versa). Re-index zettel in case
				// a hidden zettel was recovered
				mgr.idxAr.Enqueue(zid, arUpdate)
			}
			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, inverseKey, value string, zi *store.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if _, err := mgr.GetMeta(ctx, zid); err != nil {
		zi.AddDeadRef(zid)
		return
	}
	if inverseKey == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddMetaRef(inverseKey, zid)
}

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

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

Deleted 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


























































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 boxes and indexes of a Zettelstore.
package manager

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

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/memstore"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

// 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 {
		log.Fatalf("Box 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 box.
type Manager struct {
	mgrMx        sync.RWMutex
	started      bool
	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys 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
	idxDurReload   time.Duration
	idxSinceReload uint64
}

// New creates a new managing box.
func New(boxURIs []*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 box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

		idxStore: memstore.New(),
		idxAr:    newAnterooms(10),
		idxReady: make(chan struct{}, 1),
	}
	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
			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
}

// 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) notifyObserver(ci *box.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.Box == nil {
					ci.Box = mgr
				}
				mgr.notifyObserver(&ci)
			}
		case <-mgr.done:
			return
		}
	}
}

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
	switch reason {
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnUpdate:
		mgr.idxAr.Enqueue(zid, arUpdate)
	case box.OnDelete:
		mgr.idxAr.Enqueue(zid, arDelete)
	default:
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}

// Start the box. Now all other functions of the box are allowed.
// Starting an already started box is not allowed.
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mgrMx.Lock()
	if mgr.started {
		mgr.mgrMx.Unlock()
		return box.ErrStarted
	}
	for i := len(mgr.boxes) - 1; i >= 0; i-- {
		ssi, ok := mgr.boxes[i].(box.StartStopper)
		if !ok {
			continue
		}
		err := ssi.Start(ctx)
		if err == nil {
			continue
		}
		for j := i + 1; j < len(mgr.boxes); j++ {
			if ssj, ok := mgr.boxes[j].(box.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 <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	return nil
}

// Stop the started box. 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 box.ErrStopped
	}
	close(mgr.done)
	var err error
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			if err1 := ss.Stop(ctx); err1 != nil && err == nil {
				err = err1
			}
		}
	}
	mgr.started = false
	return err
}

// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	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)
}

Deleted box/manager/memstore/memstore.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580




































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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/box/manager/store"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

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

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

func (zi *zettelIndex) isEmpty() bool {
	if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 {
		return false
	}
	return 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
}

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

	addBackwardZids(result, zid, zi)
	return result
}

// SearchPrefix returns all zettel that have a word with the given prefix.
// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchPrefix(prefix string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(prefix, strings.HasPrefix)
	l := len(prefix)
	if l > 14 {
		return result
	}
	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
}

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

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

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

func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) {
	// Must only be called if ms.mx is read-locked!
	result[zid] = 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 box/manager/memstore/refs.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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 box/manager/memstore/refs_test.go.

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










































































































































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

Deleted 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



























































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

// Package store contains general index data for storing a zettel index.
package store

import (
	"context"
	"io"

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

	// Entrich metadata with data from store.
	Enrich(ctx context.Context, m *meta.Meta)

	// UpdateReferences for a specific zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	UpdateReferences(context.Context, *ZettelIndex) id.Set

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

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

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

Deleted 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





























































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














































































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

Deleted 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

























































































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








































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 membox stores zettel volatile in main memory.
package membox

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

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
)

func init() {
	manager.Register(
		"mem",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &memBox{u: u, cdata: *cdata}, nil
		})
}

type memBox struct {
	u      *url.URL
	cdata  manager.ConnectData
	zettel map[id.Zid]domain.Zettel
	mx     sync.RWMutex
}

func (mp *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) {
	if chci := mp.cdata.Notify; chci != nil {
		chci <- box.UpdateInfo{Reason: reason, Zid: zid}
	}
}

func (mp *memBox) Location() string {
	return mp.u.String()
}

func (mp *memBox) Start(ctx context.Context) error {
	mp.mx.Lock()
	mp.zettel = make(map[id.Zid]domain.Zettel)
	mp.mx.Unlock()
	return nil
}

func (mp *memBox) Stop(ctx context.Context) error {
	mp.mx.Lock()
	mp.zettel = nil
	mp.mx.Unlock()
	return nil
}

func (mp *memBox) CanCreateZettel(ctx context.Context) bool { return true }

func (mp *memBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	mp.mx.Lock()
	zid, err := box.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(box.OnUpdate, zid)
	return zid, nil
}

func (mp *memBox) 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{}, box.ErrNotFound
	}
	zettel.Meta = zettel.Meta.Clone()
	return zettel, nil
}

func (mp *memBox) 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, box.ErrNotFound
	}
	return zettel.Meta.Clone(), nil
}

func (mp *memBox) 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 *memBox) 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, mp.cdata.Number)
		if match(m) {
			result = append(result, m)
		}
	}
	mp.mx.RUnlock()
	return result, nil
}

func (mp *memBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	return true
}

func (mp *memBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	mp.mx.Lock()
	meta := zettel.Meta.Clone()
	if !meta.Zid.IsValid() {
		return &box.ErrInvalidID{Zid: meta.Zid}
	}
	zettel.Meta = meta
	mp.zettel[meta.Zid] = zettel
	mp.mx.Unlock()
	mp.notifyChanged(box.OnUpdate, meta.Zid)
	return nil
}

func (mp *memBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true }

func (mp *memBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mp.mx.Lock()
	zettel, ok := mp.zettel[curZid]
	if !ok {
		mp.mx.Unlock()
		return box.ErrNotFound
	}

	// Check that there is no zettel with newZid
	if _, ok = mp.zettel[newZid]; ok {
		mp.mx.Unlock()
		return &box.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(box.OnDelete, curZid)
	mp.notifyChanged(box.OnUpdate, newZid)
	return nil
}

func (mp *memBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	mp.mx.RLock()
	_, ok := mp.zettel[zid]
	mp.mx.RUnlock()
	return ok
}

func (mp *memBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mp.mx.Lock()
	if _, ok := mp.zettel[zid]; !ok {
		mp.mx.Unlock()
		return box.ErrNotFound
	}
	delete(mp.zettel, zid)
	mp.mx.Unlock()
	mp.notifyChanged(box.OnDelete, zid)
	return nil
}

func (mp *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mp.mx.RLock()
	st.Zettel = len(mp.zettel)
	mp.mx.RUnlock()
}

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

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

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



























































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 client provides a client for accessing the Zettelstore via its API.
package client

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

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

// Client contains all data to execute requests.
type Client struct {
	baseURL   string
	username  string
	password  string
	token     string
	tokenType string
	expires   time.Time
}

// NewClient create a new client.
func NewClient(baseURL string) *Client {
	if !strings.HasSuffix(baseURL, "/") {
		baseURL += "/"
	}
	c := Client{baseURL: baseURL}
	return &c
}

func (c *Client) newURLBuilder(key byte) *api.URLBuilder {
	return api.NewURLBuilder(c.baseURL, key)
}
func (c *Client) newRequest(ctx context.Context, method string, ub *api.URLBuilder, body io.Reader) (*http.Request, error) {
	return http.NewRequestWithContext(ctx, method, ub.String(), body)
}

func (c *Client) executeRequest(req *http.Request) (*http.Response, error) {
	if c.token != "" {
		req.Header.Add("Authorization", c.tokenType+" "+c.token)
	}
	client := http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		if resp != nil && resp.Body != nil {
			resp.Body.Close()
		}
		return nil, err
	}
	return resp, err
}

func (c *Client) buildAndExecuteRequest(
	ctx context.Context, method string, ub *api.URLBuilder, body io.Reader, h http.Header) (*http.Response, error) {
	req, err := c.newRequest(ctx, method, ub, body)
	if err != nil {
		return nil, err
	}
	err = c.updateToken(ctx)
	if err != nil {
		return nil, err
	}
	for key, val := range h {
		req.Header[key] = append(req.Header[key], val...)
	}
	return c.executeRequest(req)
}

// SetAuth sets authentication data.
func (c *Client) SetAuth(username, password string) {
	c.username = username
	c.password = password
	c.token = ""
	c.tokenType = ""
	c.expires = time.Time{}
}

func (c *Client) executeAuthRequest(req *http.Request) error {
	resp, err := c.executeRequest(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var tinfo api.AuthJSON
	err = dec.Decode(&tinfo)
	if err != nil {
		return err
	}
	c.token = tinfo.Token
	c.tokenType = tinfo.Type
	c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second)
	return nil
}

func (c *Client) updateToken(ctx context.Context) error {
	if c.username == "" {
		return nil
	}
	if time.Now().After(c.expires) {
		return c.Authenticate(ctx)
	}
	return c.RefreshToken(ctx)
}

// Authenticate sets a new token by sending user name and password.
func (c *Client) Authenticate(ctx context.Context) error {
	authData := url.Values{"username": {c.username}, "password": {c.password}}
	req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('v'), strings.NewReader(authData.Encode()))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	return c.executeAuthRequest(req)
}

// RefreshToken updates the access token
func (c *Client) RefreshToken(ctx context.Context) error {
	req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('v'), nil)
	if err != nil {
		return err
	}
	return c.executeAuthRequest(req)
}

// CreateZettel creates a new zettel and returns its URL.
func (c *Client) CreateZettel(ctx context.Context, data *api.ZettelDataJSON) (id.Zid, error) {
	var buf bytes.Buffer
	if err := encodeZettelData(&buf, data); err != nil {
		return id.Invalid, err
	}
	ub := c.jsonZettelURLBuilder(nil)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil)
	if err != nil {
		return id.Invalid, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusCreated {
		return id.Invalid, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var newZid api.ZidJSON
	err = dec.Decode(&newZid)
	if err != nil {
		return id.Invalid, err
	}
	zid, err := id.Parse(newZid.ID)
	if err != nil {
		return id.Invalid, err
	}
	return zid, nil
}

func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error {
	enc := json.NewEncoder(buf)
	enc.SetEscapeHTML(false)
	return enc.Encode(&data)
}

// ListZettel returns a list of all Zettel.
func (c *Client) ListZettel(ctx context.Context, query url.Values) ([]api.ZettelJSON, error) {
	ub := c.jsonZettelURLBuilder(query)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var zl api.ZettelListJSON
	err = dec.Decode(&zl)
	if err != nil {
		return nil, err
	}
	return zl.List, nil
}

// GetZettelJSON returns a zettel as a JSON struct.
func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid, query url.Values) (*api.ZettelDataJSON, error) {
	ub := c.jsonZettelURLBuilder(query).SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var out api.ZettelDataJSON
	err = dec.Decode(&out)
	if err != nil {
		return nil, err
	}
	return &out, nil
}

// GetEvaluatedZettel return a zettel in a defined encoding.
func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) {
	ub := c.jsonZettelURLBuilder(nil).SetZid(zid)
	ub.AppendQuery(api.QueryKeyFormat, enc.String())
	ub.AppendQuery(api.QueryKeyPart, api.PartContent)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", errors.New(resp.Status)
	}
	content, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return string(content), nil
}

// GetZettelOrder returns metadata of the given zettel and, more important,
// metadata of zettel that are referenced in a list within the first zettel.
func (c *Client) GetZettelOrder(ctx context.Context, zid id.Zid) (*api.ZidMetaRelatedList, error) {
	ub := c.newURLBuilder('o').SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var out api.ZidMetaRelatedList
	err = dec.Decode(&out)
	if err != nil {
		return nil, err
	}
	return &out, nil
}

// ContextDirection specifies how the context should be calculated.
type ContextDirection uint8

// Allowed values for ContextDirection
const (
	_ ContextDirection = iota
	DirBoth
	DirBackward
	DirForward
)

// GetZettelContext returns metadata of the given zettel and, more important,
// metadata of zettel that for the context of the first zettel.
func (c *Client) GetZettelContext(
	ctx context.Context, zid id.Zid, dir ContextDirection, depth, limit int) (
	*api.ZidMetaRelatedList, error,
) {
	ub := c.newURLBuilder('x').SetZid(zid)
	switch dir {
	case DirBackward:
		ub.AppendQuery(api.QueryKeyDir, api.DirBackward)
	case DirForward:
		ub.AppendQuery(api.QueryKeyDir, api.DirForward)
	}
	if depth > 0 {
		ub.AppendQuery(api.QueryKeyDepth, strconv.Itoa(depth))
	}
	if limit > 0 {
		ub.AppendQuery(api.QueryKeyLimit, strconv.Itoa(limit))
	}
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var out api.ZidMetaRelatedList
	err = dec.Decode(&out)
	if err != nil {
		return nil, err
	}
	return &out, nil
}

// GetZettelLinks returns connections to ohter zettel, images, externals URLs.
func (c *Client) GetZettelLinks(ctx context.Context, zid id.Zid) (*api.ZettelLinksJSON, error) {
	ub := c.newURLBuilder('l').SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var out api.ZettelLinksJSON
	err = dec.Decode(&out)
	if err != nil {
		return nil, err
	}
	return &out, nil
}

// UpdateZettel updates an existing zettel.
func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data *api.ZettelDataJSON) error {
	var buf bytes.Buffer
	if err := encodeZettelData(&buf, data); err != nil {
		return err
	}
	ub := c.jsonZettelURLBuilder(nil).SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNoContent {
		return errors.New(resp.Status)
	}
	return nil
}

// RenameZettel renames a zettel.
func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid id.Zid) error {
	ub := c.jsonZettelURLBuilder(nil).SetZid(oldZid)
	h := http.Header{
		api.HeaderDestination: {c.jsonZettelURLBuilder(nil).SetZid(newZid).String()},
	}
	resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNoContent {
		return errors.New(resp.Status)
	}
	return nil
}

// DeleteZettel deletes a zettel with the given identifier.
func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error {
	ub := c.jsonZettelURLBuilder(nil).SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNoContent {
		return errors.New(resp.Status)
	}
	return nil
}

func (c *Client) jsonZettelURLBuilder(query url.Values) *api.URLBuilder {
	ub := c.newURLBuilder('z')
	for key, values := range query {
		if key == api.QueryKeyFormat {
			continue
		}
		for _, val := range values {
			ub.AppendQuery(key, val)
		}
	}
	return ub
}

// ListTags returns a map of all tags, together with the associated zettel containing this tag.
func (c *Client) ListTags(ctx context.Context) (map[string][]string, error) {
	err := c.updateToken(ctx)
	if err != nil {
		return nil, err
	}
	req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('t'), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.executeRequest(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var tl api.TagListJSON
	err = dec.Decode(&tl)
	if err != nil {
		return nil, err
	}
	return tl.Tags, nil
}

// ListRoles returns a list of all roles.
func (c *Client) ListRoles(ctx context.Context) ([]string, error) {
	err := c.updateToken(ctx)
	if err != nil {
		return nil, err
	}
	req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('r'), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.executeRequest(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}
	dec := json.NewDecoder(resp.Body)
	var rl api.RoleListJSON
	err = dec.Decode(&rl)
	if err != nil {
		return nil, err
	}
	return rl.Roles, nil
}

Deleted 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














































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 client provides a client for accessing the Zettelstore via its API.
package client_test

import (
	"context"
	"flag"
	"fmt"
	"net/url"
	"testing"

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

func TestCreateRenameDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("creator", "creator")
	zid, err := c.CreateZettel(context.Background(), &api.ZettelDataJSON{
		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 := zid + 1
	c.SetAuth("owner", "owner")
	err = c.RenameZettel(context.Background(), zid, newZid)
	if err != nil {
		t.Error("Cannot rename", zid, ":", err)
		newZid = zid
	}
	err = c.DeleteZettel(context.Background(), newZid)
	if err != nil {
		t.Error("Cannot delete", zid, ":", err)
		return
	}
}

func TestUpdateZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("writer", "writer")
	z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil)
	if err != nil {
		t.Error(err)
		return
	}
	if got := z.Meta[meta.KeyTitle]; got != "Home" {
		t.Errorf("Title of zettel is not \"Home\", but %q", got)
		return
	}
	newTitle := "New Home"
	z.Meta[meta.KeyTitle] = newTitle
	err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, z)
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil)
	if err != nil {
		t.Error(err)
		return
	}
	if got := zt.Meta[meta.KeyTitle]; got != newTitle {
		t.Errorf("Title of zettel is not %q, but %q", newTitle, got)
	}
}

func TestList(t *testing.T) {
	testdata := []struct {
		user string
		exp  int
	}{
		{"", 7},
		{"creator", 10},
		{"reader", 12},
		{"writer", 12},
		{"owner", 34},
	}

	t.Parallel()
	c := getClient()
	query := url.Values{api.QueryKeyFormat: {"html"}} // Client must remove "html"
	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)
			l, err := c.ListZettel(context.Background(), query)
			if err != nil {
				tt.Error(err)
				return
			}
			got := len(l)
			if got != tc.exp {
				tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
			}
		})
	}
	l, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}})
	if err != nil {
		t.Error(err)
		return
	}
	got := len(l)
	if got != 27 {
		t.Errorf("List of length %d expected, but got %d\n%v", 27, got, l)
	}
}
func TestGetZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, url.Values{api.QueryKeyPart: {api.PartContent}})
	if err != nil {
		t.Error(err)
		return
	}
	if m := z.Meta; len(m) > 0 {
		t.Errorf("Exptected 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)
	}
}

func TestGetEvaluatedZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	encodings := []api.EncodingEnum{
		api.EncoderDJSON,
		api.EncoderHTML,
		api.EncoderNative,
		api.EncoderText,
	}
	for _, enc := range encodings {
		content, err := c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for encoding %v", enc)
		}
	}
}

func TestGetZettelOrder(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.GetZettelOrder(context.Background(), id.TOCNewTemplateZid)
	if err != nil {
		t.Error(err)
		return
	}
	if rl.ID != id.TOCNewTemplateZid.String() {
		t.Errorf("Expected an Zid %v, but got %v", id.TOCNewTemplateZid, rl.ID)
		return
	}
	l := rl.List
	if got := len(l); got != 2 {
		t.Errorf("Expected list fo length 2, got %d", got)
		return
	}
	if got := l[0].ID; got != id.TemplateNewZettelZid.String() {
		t.Errorf("Expected result[0]=%v, but got %v", id.TemplateNewZettelZid, got)
	}
	if got := l[1].ID; got != id.TemplateNewUserZid.String() {
		t.Errorf("Expected result[1]=%v, but got %v", id.TemplateNewUserZid, got)
	}
}

func TestGetZettelContext(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.GetZettelContext(context.Background(), id.VersionZid, client.DirBoth, 0, 3)
	if err != nil {
		t.Error(err)
		return
	}
	if rl.ID != id.VersionZid.String() {
		t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID)
		return
	}
	l := rl.List
	if got := len(l); got != 3 {
		t.Errorf("Expected list fo length 3, got %d", got)
		return
	}
	if got := l[0].ID; got != id.DefaultHomeZid.String() {
		t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got)
	}
	if got := l[1].ID; got != id.OperatingSystemZid.String() {
		t.Errorf("Expected result[1]=%v, but got %v", id.OperatingSystemZid, got)
	}
	if got := l[2].ID; got != id.StartupConfigurationZid.String() {
		t.Errorf("Expected result[2]=%v, but got %v", id.StartupConfigurationZid, got)
	}

	rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0)
	if err != nil {
		t.Error(err)
		return
	}
	if rl.ID != id.VersionZid.String() {
		t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID)
		return
	}
	l = rl.List
	if got := len(l); got != 1 {
		t.Errorf("Expected list fo length 1, got %d", got)
		return
	}
	if got := l[0].ID; got != id.DefaultHomeZid.String() {
		t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got)
	}
}

func TestGetZettelLinks(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	zl, err := c.GetZettelLinks(context.Background(), id.DefaultHomeZid)
	if err != nil {
		t.Error(err)
		return
	}
	if zl.ID != id.DefaultHomeZid.String() {
		t.Errorf("Expected an Zid %v, but got %v", id.DefaultHomeZid, zl.ID)
		return
	}
	if len(zl.Links.Incoming) != 0 {
		t.Error("No incomings expected", zl.Links.Incoming)
	}
	if got := len(zl.Links.Outgoing); got != 4 {
		t.Errorf("Expected 4 outgoing links, got %d", got)
	}
	if got := len(zl.Links.Local); got != 1 {
		t.Errorf("Expected 1 local link, got %d", got)
	}
	if got := len(zl.Links.External); got != 4 {
		t.Errorf("Expected 4 external link, got %d", got)
	}
}

func TestListTags(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	tm, err := c.ListTags(context.Background())
	if err != nil {
		t.Error(err)
		return
	}
	tags := []struct {
		key  string
		size int
	}{
		{"#invisible", 1},
		{"#user", 4},
		{"#test", 4},
	}
	if len(tm) != len(tags) {
		t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm)
	}
	for _, tag := range tags {
		if zl, ok := tm[tag.key]; !ok {
			t.Errorf("No tag %v: %v", tag.key, tm)
		} else if len(zl) != tag.size {
			t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
		}
	}
	for i, id := range tm["#user"] {
		if id != tm["#test"][i] {
			t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"])
		}
	}
}

func TestListRoles(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.ListRoles(context.Background())
	if err != nil {
		t.Error(err)
		return
	}
	exp := []string{"configuration", "user", "zettel"}
	if len(rl) != len(exp) {
		t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl)
	}
	for i, id := range exp {
		if id != rl[i] {
			t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i])
		}
	}
}

var baseURL string

func init() {
	flag.StringVar(&baseURL, "base-url", "", "Base URL")
}

func getClient() *client.Client { return client.NewClient(baseURL) }

// TestMain controls whether client API tests should run or not.
func TestMain(m *testing.M) {
	flag.Parse()
	if baseURL != "" {
		m.Run()
	}
}

Changes to cmd/cmd_file.go.

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

19
20
21
22
23
24
25







-








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

	"zettelstore.de/z/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)
37
38
39
40
41
42
43
44

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

43
44
45
46
47
48
49
50







-
+







		domain.Zettel{
			Meta:    m,
			Content: domain.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk),
		nil,
	)
	enc := encoder.Create(api.Encoder(format), &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)})
	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)
		return 2, nil
	}
	_, err = enc.WriteZettel(os.Stdout, z, format != "raw")
	if err != nil {
		return 2, err

Changes to cmd/cmd_run.go.

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

23

24
25
26
27
28
29
30
10
11
12
13
14
15
16

17

18
19
20
21
22
23
24
25
26
27
28
29
30







-

-



+

+








package cmd

import (
	"flag"
	"net/http"

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

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

54
55
56
57
58
59
60
61
62


63
64

65
66
67
68



69
70

71
72
73
74
75




76
77
78
79
80

81
82
83
84




85
86
87
88


89
90
91
92
93


94
95


96
97
98
99
100
101
102
103
104

105
106
107
108
109

110
111
112
113
114
115
116

117
118
119
120
121

122
123

124
125
126
127
128
129
130
131

132
133
134

135
136
54
55
56
57
58
59
60


61
62
63

64
65



66
67
68


69
70




71
72
73
74



75

76


77

78
79
80
81
82
83
84

85
86
87
88
89
90

91
92
93

94
95
96
97
98
99
100
101
102
103

104
105
106
107
108

109

110
111

112
113

114
115
116



117
118

119
120
121






122

123

124
125
126







-
-
+
+

-
+

-
-
-
+
+
+
-
-
+

-
-
-
-
+
+
+
+
-
-
-

-
+
-
-

-
+
+
+
+



-
+
+




-
+
+

-
+
+








-
+




-
+
-


-


-
+


-
-
-
+

-
+


-
-
-
-
-
-
+
-

-
+


	kern.SetDebug(debug)
	if err := kern.StartService(kernel.WebService); err != nil {
		return 1, err
	}
	return 0, nil
}

func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) {
	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig)
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, boxManager, authPolicy)
	wui := webui.New(webSrv, authManager, rtConfig, authManager, placeManager, authPolicy)

	ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, boxManager)
	ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedBoxManager)
	ucGetMeta := usecase.NewGetMeta(protectedBoxManager)
	ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, placeManager)
	ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedPlaceManager)
	ucGetMeta := usecase.NewGetMeta(protectedPlaceManager)
	ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedPlaceManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucListMeta := usecase.NewListMeta(protectedBoxManager)
	ucListRoles := usecase.NewListRole(protectedBoxManager)
	ucListTags := usecase.NewListTags(protectedBoxManager)
	ucZettelContext := usecase.NewZettelContext(protectedBoxManager)
	ucListMeta := usecase.NewListMeta(protectedPlaceManager)
	ucListRoles := usecase.NewListRole(protectedPlaceManager)
	ucListTags := usecase.NewListTags(protectedPlaceManager)
	ucZettelContext := usecase.NewZettelContext(protectedPlaceManager)
	ucDelete := usecase.NewDeleteZettel(protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(protectedBoxManager)
	ucRename := usecase.NewRenameZettel(protectedBoxManager)

	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
	webSrv.Handle("/", wui.MakeGetRootHandler(protectedPlaceManager))

	// Web user interface
	webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler())
	webSrv.AddListRoute('a', http.MethodPost, wui.MakePostLoginHandler(ucAuthenticate))
	webSrv.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler(
		api.MakePostLoginHandlerAPI(ucAuthenticate),
		wui.MakePostLoginHandlerHTML(ucAuthenticate)))
	webSrv.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler())
	webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler())
	if !authManager.IsReadonly() {
		webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta))
		webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler(ucRename))
		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(ucDelete))
		webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler(
			usecase.NewDeleteZettel(protectedPlaceManager)))
		webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(ucUpdate))
		webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(
			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(
		usecase.NewSearch(protectedBoxManager), ucGetMeta, ucGetZettel))
		usecase.NewSearch(protectedPlaceManager), ucGetMeta, ucGetZettel))
	webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler(
		ucListMeta, ucListRoles, ucListTags))
	webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler(
		ucParseZettel, ucGetMeta))
	webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(
	webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(ucParseZettel, ucGetMeta))
		ucParseZettel, ucGetMeta, ucGetAllMeta))
	webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext))

	// API
	webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel))
	webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler(
		usecase.NewZettelOrder(protectedBoxManager, ucParseZettel)))
		usecase.NewZettelOrder(protectedPlaceManager, ucParseZettel)))
	webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles))
	webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags))
	webSrv.AddListRoute('v', http.MethodPost, api.MakePostLoginHandler(ucAuthenticate))
	webSrv.AddListRoute('v', http.MethodPut, api.MakeRenewAuthHandler())
	webSrv.AddZettelRoute('x', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext))
	webSrv.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext))
	webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler(
		usecase.NewListMeta(protectedBoxManager), ucGetMeta, ucParseZettel))
		usecase.NewListMeta(protectedPlaceManager), ucGetMeta, ucParseZettel))
	webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler(
		ucParseZettel, ucGetMeta))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('z', http.MethodPost, api.MakePostCreateZettelHandler(ucCreateZettel))
		webSrv.AddZettelRoute('z', http.MethodDelete, api.MakeDeleteZettelHandler(ucDelete))
		webSrv.AddZettelRoute('z', http.MethodPut, api.MakeUpdateZettelHandler(ucUpdate))
		webSrv.AddZettelRoute('z', zsapi.MethodMove, api.MakeRenameZettelHandler(ucRename))
	}


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

Changes to cmd/command.go.

15
16
17
18
19
20
21
22
23
24
25




26
27
28


29
30
31
32
33
34
35
15
16
17
18
19
20
21




22
23
24
25



26
27
28
29
30
31
32
33
34







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







	"sort"

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

// 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
	Boxes      bool                // if true then boxes will be set up
	Header     bool                // Print a heading on startup
	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
	LineServer bool                // Start admin line server
	Flags      func(*flag.FlagSet) // function to set up flag.FlagSet
	flags      *flag.FlagSet       // flags that belong to the command
	Flags  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)

Changes to cmd/fd_limit_raise.go.

37
38
39
40
41
42
43
44

45
46
47
37
38
39
40
41
42
43

44
45
46
47







-
+



		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 boxes if you enabled notification\n", rLimit.Cur)
		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.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32



33
34
35
36
37
38
39
18
19
20
21
22
23
24



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







-
-
-





+
+
+







	"net/url"
	"os"
	"strconv"
	"strings"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/impl"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/compbox"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"
	"zettelstore.de/z/place/progplace"
	"zettelstore.de/z/web/server"
)

const (
	defConfigfile = ".zscfg"
)

50
51
52
53
54
55
56
57
58
59
60




61
62

63
64
65
66
67

68
69
70
71
72
73
74
50
51
52
53
54
55
56




57
58
59
60


61
62
63
64
65

66
67
68
69
70
71
72
73







-
-
-
-
+
+
+
+
-
-
+




-
+







	})
	RegisterCommand(Command{
		Name:   "version",
		Func:   func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil },
		Header: true,
	})
	RegisterCommand(Command{
		Name:       "run",
		Func:       runFunc,
		Boxes:      true,
		Header:     true,
		Name:   "run",
		Func:   runFunc,
		Places: true,
		Header: true,
		LineServer: true,
		Flags:      flgRun,
		Flags:  flgRun,
	})
	RegisterCommand(Command{
		Name:   "run-simple",
		Func:   runSimpleFunc,
		Boxes:  true,
		Places: true,
		Header: true,
		Flags:  flgSimpleRun,
	})
	RegisterCommand(Command{
		Name: "file",
		Func: cmdFile,
		Flags: func(fs *flag.FlagSet) {
110
111
112
113
114
115
116
117

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148












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
109
110
111
112
113
114
115

116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135












136
137
138
139
140
141
142
143
144
145
146
147
148
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







-
+



















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












-
-
-
-
+
+
+
+






-
+







		case "d":
			val := flg.Value.String()
			if strings.HasPrefix(val, "/") {
				val = "dir://" + val
			} else {
				val = "dir:" + val
			}
			cfg.Set(keyBoxOneURI, val)
			cfg.Set(keyPlaceOneURI, val)
		case "r":
			cfg.Set(keyReadOnly, flg.Value.String())
		case "v":
			cfg.Set(keyVerbose, flg.Value.String())
		}
	})
	return cfg
}

func parsePort(s string) (string, error) {
	port, err := net.LookupPort("tcp", s)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s)
		return "", err
	}
	return strconv.Itoa(port), nil
}

const (
	keyAdminPort         = "admin-port"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyListenAddr        = "listen-addr"
	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"
	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"
)

func setServiceConfig(cfg *meta.Meta) error {
	ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
	if val, found := cfg.Get(keyAdminPort); found {
		ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val)
	}

	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, ""))
	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly))

	ok = setConfigValue(
		ok, kernel.BoxService, kernel.BoxDefaultDirType,
		cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify))
	ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel")
	format := kernel.BoxURIs + "%v"
		ok, kernel.PlaceService, kernel.PlaceDefaultDirType,
		cfg.GetDefault(keyDefaultDirPlaceType, kernel.PlaceDirTypeNotify))
	ok = setConfigValue(ok, kernel.PlaceService, kernel.PlaceURIs+"1", "dir:./zettel")
	format := kernel.PlaceURIs + "%v"
	for i := 1; ; i++ {
		key := fmt.Sprintf(format, i)
		val, found := cfg.Get(key)
		if !found {
			break
		}
		ok = setConfigValue(ok, kernel.BoxService, key, val)
		ok = setConfigValue(ok, kernel.PlaceService, 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))
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
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







-
-
-
+
+
+





-
+

-
-
-
+
+
+


-
+







-
+







	done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val))
	if !done {
		kernel.Main.Log("unable to set configuration:", key, val)
	}
	return ok && done
}

func setupOperations(cfg *meta.Meta, withBoxes bool) {
	var createManager kernel.CreateBoxManagerFunc
	if withBoxes {
func setupOperations(cfg *meta.Meta, withPlaces bool) {
	var createManager kernel.CreatePlaceManagerFunc
	if withPlaces {
		err := raiseFdLimit()
		if err != nil {
			srvm := kernel.Main
			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.BoxService, kernel.BoxDefaultDirType, kernel.BoxDirTypeSimple)
			srvm.SetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType, kernel.PlaceDirTypeSimple)
		}
		createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) {
			compbox.Setup(cfg)
			return manager.New(boxURIs, authManager, rtConfig)
		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) (box.Manager, error) { return nil, nil }
		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 box.Manager, authMgr auth.Manager, rtConfig config.Config) error {
		func(srv server.Server, plMgr place.Manager, authMgr auth.Manager, rtConfig config.Config) error {
			setupRouting(srv, plMgr, authMgr, rtConfig)
			return nil
		},
	)
}

func executeCommand(name string, args ...string) int {
238
239
240
241
242
243
244
245
246


247
248
249
250
251
252
253
237
238
239
240
241
242
243


244
245
246
247
248
249
250
251
252







-
-
+
+







		return 1
	}
	cfg := getConfig(fs)
	if err := setServiceConfig(cfg); err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
		return 2
	}
	setupOperations(cfg, command.Boxes)
	kernel.Main.Start(command.Header, command.LineServer)
	setupOperations(cfg, command.Places)
	kernel.Main.Start(command.Header)
	exitCode, err := command.Func(fs, cfg)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
	}
	kernel.Main.Shutdown(true)
	return exitCode
}

Changes to cmd/register.go.

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





33
9
10
11
12
13
14
15





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







-
-
-
-
-












+
+
+
+
+

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

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



































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

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



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68














69
70
71
72
73
74
75
76
77
78
79
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 collect provides functions to collect items from a syntax tree.
package collect

import (
import "zettelstore.de/z/ast"
	"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
	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) (s Summary) {
	ast.WalkBlockSlice(&s, zn.Ast)
	return s
func References(zn *ast.ZettelNode) Summary {
	lv := linkVisitor{}
	ast.NewTopDownTraverser(&lv).VisitBlockSlice(zn.Ast)
	return lv.summary
}

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.
func (lv *linkVisitor) VisitTag(tn *ast.TagNode) {}
// Visit all node to collect data for the summary.
func (s *Summary) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.LinkNode:
		s.Links = append(s.Links, n.Ref)
	case *ast.ImageNode:
		if n.Ref != nil {
			s.Images = append(s.Images, n.Ref)
		}
	case *ast.CiteNode:
		s.Cites = append(s.Cites, n)
	}
	return s
}

// VisitSpace does nothing.
func (lv *linkVisitor) VisitSpace(sn *ast.SpaceNode) {}

// VisitBreak does nothing.
func (lv *linkVisitor) VisitBreak(bn *ast.BreakNode) {}

// VisitLink collects the given link as a reference.
func (lv *linkVisitor) VisitLink(ln *ast.LinkNode) {
	lv.summary.Links = append(lv.summary.Links, ln.Ref)
}

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

// VisitCite collects the citation.
func (lv *linkVisitor) VisitCite(cn *ast.CiteNode) {
	lv.summary.Cites = append(lv.summary.Cites, cn)
}

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

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
23
24
25
26
27
28
29

30
31
32
33
34
35
36







-







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

	intNode := &ast.LinkNode{Ref: parseRef("01234567890123")}
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
50
51
52
53
54
55
56

57
58
59
60
61
62
63







-







	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) {
	t.Parallel()
	zn := &ast.ZettelNode{
		Ast: ast.BlockSlice{
			&ast.ParaNode{
				Inlines: ast.InlineSlice{
					&ast.ImageNode{Ref: parseRef("12345678901234")},
				},
			},

Changes to collect/order.go.

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27







-
+








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.Kind {
			switch ln.Code {
			case ast.NestedListOrdered, ast.NestedListUnordered:
				for _, is := range ln.Items {
					if ref := firstItemZettelReference(is); ref != nil {
						result = append(result, ref)
					}
				}
			}

Changes to config/config.go.

52
53
54
55
56
57
58




59
60
61
62
63
64
65
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69







+
+
+
+








	// 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 {
	// GetExpertMode returns the current value of the "expert-mode" key
	GetExpertMode() bool

Changes to docs/manual/00001002000000.zettel.

11
12
13
14
15
16
17
18

19
20
21

22
23
24
25
26
27
28
29
30
31
11
12
13
14
15
16
17

18
19
20

21
22
23
24
25
26
27
28
29
30
31







-
+


-
+










: It should be not hard to write other software that works with your zettel.
; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.
  If your device is securely configured, there should be no risk that others are able to read or update your zettel.
: If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel.
; Ease of installation
: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working.
: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate place and start working.
: Upgrading the software is done just by replacing the executable with a newer one.
; Ease of operation
: There is only one executable for Zettelstore and one directory, where your zettel are stored.
: There is only one executable for Zettelstore and one directory, where your zettel are placed.
: 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.
  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.

Changes to docs/manual/00001003000000.zettel.

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

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

12
13
14
15
16
17
18
19











-
+







id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

=== 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 put the executable.
* A sub-directory ""zettel"" will be created in the directory where you placed 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.
* If you restart your device, please make sure to start your Zettelstore again.

39
40
41
42
43
44
45
46

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

46
47
48
49
50
51
52
53







-
+







```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'':
Create a systemd service file and place it into ''/etc/systemd/system/zettelstore.service'':
```ini
[Unit]
Description=Zettelstore
After=network.target

[Service]
Type=simple

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

6
7
8
9

10
11
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: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210712234656
modified: 20210525121644

The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options.
These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons.
For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored.
For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are placed.
An attacker that is able to change the owner can do anything.
Therefore only the owner of the computer on which Zettelstore runs can change this information.

The file for startup configuration must be created via a text editor in advance.

The syntax of the configuration file is the same as for any zettel metadata.
The following keys are supported:

; [!admin-port]''admin-port''
: Specifies the TCP port through which you can reach the [[administrator console|00001004100000]].
  A value of ''0'' (the default) disables the administrator console.
  A value of ''0'' (the default) disables the administrators console.
  The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]].

  On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended).

  Default: ''0''
; [!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.
; [!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-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: ''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).
  Otherwise web browser are free to ignore the authentication cookie.

  Default: ''false''
56
57
58
59
60
61
62







63
64
65
66
67
68
69
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68







+
+
+
+
+
+
+







  On these devices, the operating system is free to stop the web browser and to remove temporary cookies.
  Therefore, an authenticated user will be logged off.

  If ''true'', a persistent cookie is used.
  Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds.

  Default: ''false''
; [!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.
  No changes are possible.
  Default: false.
; [!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.
79
80
81
82
83
84
85


78
79
80
81
82
83
84
85
86







+
+
  Must begin and end with a slash character (""''/''"", ''U+002F'').
  Default: ''"/"''.

  This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore.
; ''verbose''
: Be more verbose inf logging data.
  Default: false

Other keys will be ignored.

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

-
+







-
+





-
-
-
+
+
+

-
+








-
+




-
+




-
-
-
-
+
+
+
+

-
-
+
+

id: 00001004011200
title: Zettelstore boxes
title: Zettelstore places
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210525121452

A Zettelstore must store its zettel somehow and somewhere.
In most cases you want to store your zettel as files in a directory.
Under certain circumstances you may want to store your zettel elsewhere.
Under certain circumstances you may want to store your zettel in other places.

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

The following box URIs are supported:
The following place URIs are supported:

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

  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 box.
  It is possible to [[configure|00001004011400]] a directory place.
; ''file:FILE.zip'' oder ''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 box is always read-only.
  This place is always read-only.
; ''mem:''
: Stores all its zettel in volatile memory.
  If you stop the Zettelstore, all changes are lost.

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

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.
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.
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
60
61
62
63
64

65
66
67
68
69
70


71
72
73
74
75
76


77
78
79

80
81

82

-
+





-
-
+
+




-
+







-
+

-
+



















-
+








-
+

-
+









-
+





-
-
+
+




-
-
+
+

-
+

-
+
id: 00001004011400
title: Configure file directory boxes
title: Configure file directory places
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210525121232

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

The following parameters are supported:

|= Parameter:|Description|Default value:|
|type|(Sub-) Type of the directory service|(value of ''[[default-dir-box-type|00001004010000#default-dir-box-type]]'')
|type|(Sub-) Type of the directory service|(value of ''[[default-dir-place-type|00001004010000#default-dir-place-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

=== 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 put 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 placed on an external service, such as a file server accessed via SMD/CIFS or NFS.

To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box.
To cope with this uncertainty, Zettelstore provides various internal implementations of a directory place.
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).
; 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.
```
box-uri-1: dir:///home/zettel?rescan=300
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 is preferred.
Therefore a large value if preferred.

This value is ignored for other directory box types, such as ""simple"".
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 box, only a limited number of parallel accesses are desirable.
Depending on the hardware and on the type of the directory place, 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 box of type ""notify"", the default value is: 7.
The directory box type ""simple"" limits the value to a maximum of 1, i.e. no concurrency is possible with this type of directory box.
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.

=== Readonly
Sometimes you may want to provide zettel from a file directory box, but you want to disallow any changes.
If you provide the query parameter ''readonly'' (with or without a corresponding value), the box will disallow any changes.
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.
```
box-uri-1: dir:///home/zettel?readonly
place-uri-1: dir:///home/zettel?readonly
```
If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory boxes will be in read-only mode too, even if not explicitly configured.
If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory places will be in read-only mode too, even if not explicitly configured.

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





-







id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210611213730

You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called ""configuration zettel"".
The following metadata keys change the appearance / behavior of Zettelstore:

; [!default-copyright]''default-copyright''
: Copyright value to be used when rendering content.
51
52
53
54
55
56
57




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







+
+
+
+







  Default: (the empty string).
; [!home-zettel]''home-zettel''
: Specifies the identifier of the zettel, that should be presented for the default view / home view.
  If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown.
; [!marker-external]''marker-external''
: Some HTML code that is displayed after a reference to external material.
  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''
: 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''.

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

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





-
+







+







id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210712233414
precursor: 00001004050000

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
1
2
3
4
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: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210712234031
precursor: 00001004050000

Emits some information about the Zettelstore's version.
This allows you to check, whether your installed Zettelstore is 

The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer.
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 build version information is a string like ''1.0.2+351ae138b4''.
The part ""1.0.2"" is the release version.
""+351ae138b4"" is a code uniquely identifying the version to the developer.
The build version information is a string like ''v1.0.2-34-gf567a3''.
The part ""v1.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.

Everything after the release version is optional, eg. ""1.4.3"" is a valid build version information too.
Everything after the release version is optional, eg. ""v1.4.3"" is a valid build version information too.

Example:

```
# zettelstore version
Zettelstore 1.0.2+351ae138b4 (go1.16.5@linux/amd64)
Zettelstore (v0.0.4/go1.15) running on mycomputer (linux/amd64)
Licensed under the latest version of the EUPL (European Union Public License)
```
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]].
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]].
It runs on a computer named ""mycomputer"".
The software was build for running under a Linux operating system with an ""amd64"" processor.

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

6
7
8
9
10
11
12

13
14
15
16
17
18
19
20





-
+
+





-
+







id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210712234419
modified: 20210510153318
precursor: 00001004050000

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]
zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v]
```

; [!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''
: Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read.

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

6
7
8
9
10
11
12
13

14

15
16
17
18
19
20
21
22
23





-
+







-
+
-









id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210712234203
precursor: 00001004050000

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

It allows only to specify a zettel directory.
The directory will be created automatically, if it does not exist.
This is a difference to the ''run'' sub-command, where the directory must exists.
This is the only 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''
: 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
1
2
3
4
5

6
7
8
9
10
11
12
13





-
+







id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210712234222
precursor: 00001004050000

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

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
modified: 20210712234305
precursor: 00001004050000

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 some background information.
See [[Creating an user zettel|00001010040200]] for background.

An example:

```
# zettelstore password bob 20200911115600
Password:
   Again:

Changes to docs/manual/00001004101000.zettel.

56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71







-
+








: Displays s list of all available services and their current status.
; ''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 box box-uri-0 any_text`` will remove all values of the list //box-uri-//.
  E.g. ``set-config place place-uri-0 any_text`` will remove all values of the list //place-uri-//.
; ''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''
: Display some statistical values for the given service.
; ''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
1
2
3
4
5

6
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: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20210614165848

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

=== Where zettel are stored

Your zettel are stored typically as files in a specific directory.
Your zettel are stored 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'').
This allows zettel to be sorted naturally by creation time.
45
46
47
48
49
50
51
52

53
54
55
56
57
58
59

60
61
62
63
64
65

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
44
45
46
47
48
49
50

51
52
53
54
55
56
57

58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
74
75
76



















-
+






-
+





-
+












-
-
-
-
-
-
-
-
-
-
-
-
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.
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 put 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 placed 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 a broken image is a predefined GIF image.
The icon that visualizes an external link is a predefined SVG image.
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 executable file to use Zettelstore.
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.].

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: other 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 memeory (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
41
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
38
39
40





-
+










-
+













-
-
+









id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
modified: 20210622124647
modified: 20210511180816

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
| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process
| [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places and the the index process
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[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
| [[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
| [[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 | 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]]
| [[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]]""
| [[00010000000000]] | Home | Default home zettel, contains some welcome information

If a zettel is not linked, it is not accessible for the current user.

**Important:** All identifier may change until a stable version of the software is released.

Changes to docs/manual/00001006020000.zettel.

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

6
7
8
9
10
11
12





-







id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20210709162756

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

; [!back]''back''
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

61
62
63
64

65
66
67
68
69
70
71
44
45
46
47
48
49
50



51
52
53
54
55

56
57
58
59

60
61
62
63
64
65
66
67







-
-
-





-
+



-
+







: 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.
; [!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]].
; [!precursor]''precursor''
: References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel.
  Basically the inverse of key [[''folge''|#folge]].
; [!published]''published''
: This property contains the timestamp of the mast modification / creation of the zettel.
  If [[''modified''|#modified]] is set, it contains the same value.
  If [[''modified''|#modified]]is set, it contains the same 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|00001012052000]] zettel based on their publication date.
  It can be used for [[sorting|00001012051800#sort]] zettel based on their publication date.

  It is a computed value.
  There is no need to set it via Zettelstore.
; [!read-only]''read-only''
: Marks a zettel as read-only.
  The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not.
; [!role]''role''

Changes to docs/manual/00001006020400.zettel.

12
13
14
15
16
17
18
19

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

39
40
12
13
14
15
16
17
18

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

38
39
40







-
+


















-
+



=== 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 box|00001004011400]],
However, if the zettel is stored as a file in a [[directory place|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 box|00001004011400]],
However, if the zettel is accessible as a file in a [[directory place|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
1
2
3
4
5

6

7
8
9

10






11
12
13
14
15
16
17





-

-
+

+
-
+
-
-
-
-
-
-







id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20210627170437

All [[supported metadata keys|00001006020000]] conform to a type.
Most [[supported metadata keys|00001006020000]] conform to a type.

Every metadata key should conform to a type.
User-defined metadata keys conform also to a type, based on the suffix of the key.
User-defined metadata keys are of type EString.
|=Suffix|Type
| ''-number'' | [[Number|00001006033000]]
| ''-url'' | [[URL|00001006035000]]
| ''-zid''  | [[Identifier|00001006032000]]
| 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.

* [[Boolean|00001006030500]]

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


-


-
+
















-
+
-
-
+




id: 00001006050000
title: Zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20210721123222
role: manual

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

Deleted 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











































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20210721125518

[[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 | Zettelstore manual
| 00000200000000 | 0000899999999 | Reserved for future use
| 00009000000000 | 0000999999999 | Reserved for applications

This list may change in the future.

==== External Applications
|= From | To | Description
| 00009000001000 | 00009000001000 | ZS Slides, an application to display zettel as a HTML-based slideshow

Changes to docs/manual/00001007010000.zettel.

20
21
22
23
24
25
26
27

28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
20
21
22
23
24
25
26

27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51







-
+






-
+









-
+








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 put at the end of non-space text.].
The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If placed 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.
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 put between an ampersand character and a semicolon: ``&...;``{=zmk}.
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 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 put near the corresponding elements by using the syntax ``{...}``{=zmk}.
Attributes resemble roughly HTML attributes and are placed 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/00001007030200.zettel.

86
87
88
89
90
91
92
93

94
95
96
97
98
99
100
86
87
88
89
90
91
92

93
94
95
96
97
98
99
100







-
+







*# A.3
* B

* C
:::

Please note that two lists cannot be separated by an empty line.
Instead you should put a horizonal rule (""thematic break"") between them.
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:
```zmk
# One
# Two
[!sep]
# Uno
# Due

Changes to docs/manual/00001007031000.zettel.

89
90
91
92
93
94
95
96

97
98
99
100


101
102
103
104
105
106


107
89
90
91
92
93
94
95

96
97
98


99
100
101
102
103
104


105
106
107







-
+


-
-
+
+




-
-
+
+

|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.
Such tables are emitted by some commands of the [[administrator console|00001004100000]].
For example, the command ``get-config box`` will emit
For example, the command ``get-config place`` will emit
```
|=Key        | Value  | Description
|%-----------+--------+---------------------------
| defdirtype | notify | Default directory box type
|%-----------+--------+-----------------------------
| defdirtype | notify | Default directory place type
```
This is rendered in HTML as:
:::example
|=Key        | Value  | Description
|%-----------+--------+---------------------------
| defdirtype | notify | Default directory box type
|%-----------+--------+-----------------------------
| defdirtype | notify | Default directory place type
:::

Changes to docs/manual/00001007040000.zettel.

43
44
45
46
47
48
49
50

51
52
53
54
55
56

57
58
59
60
61
62
63
64
65
66
67
43
44
45
46
47
48
49

50
51
52
53
54
55

56
57
58
59
60
61
62
63
64
65
66
67







-
+





-
+











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, put it between these two character.
If you know the HTML name of the character you want to enter, place it between these two character.
Example: ``&amp;`` is rendered as ::&amp;::{=example}.

If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10.
Example: ``&#38;`` is rendered in HTML as ::&#38;::{=example}.

You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character.
You also can enter its numeric code point as a hex number, if you place the letter ""x"" after the numeric sign character.
Example: ``&#x26;`` is rendered in HTML as ::&#x26;::{=example}.

Since some Unicode character are used quite often, a special notation is introduced for them:

* Two consecutive hyphen-minus characters result in an //en-dash// character.
  It is typically used in numeric ranges.
  ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}.
  Alternative specifications are: ``&ndash;``, ``&x8211``, and ``&#x2013``.
* 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/00001008000000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
modified: 20210705111758
modified: 20210523194915

[[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
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

40
41
42
43
44
45
46
21
22
23
24
25
26
27






28
29
30
31
32

33
34
35
36
37
38
39
40







-
-
-
-
-
-





-
+







The following syntax values are supported:

; [!css]''css''
: A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML.
; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png''
: The formats for pixel graphics.
  Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file.
; [!html]''html''
: Hypertext Markup Language, will not be parsed further.
  Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]).

  For security reasons, equivocal elements will not be encoded in the HTML format / web user interface, e.g. the ``<script ...`` tag.
  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.
  See [[Use Markdown within Zettelstore|00001008010000]].
; [!mustache]''mustache''
: A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]].
: A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML.
; [!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/]].

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





-





-
+


-
+

-
-
-










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

Every user is associated with some basic privileges.
These are specified in the user zettel 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/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
1
2

3
4

5
6
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: 00001010070600
title: Access rules
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
modified: 20210702165416
role: manual

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.

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

   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.

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





-







id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210721120820

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.
27
28
29
30
31
32
33
34

35
36
37
38
39

40
41
42
43
44

45
46
47
48
49
50
51



26
27
28
29
30
31
32

33





34
35
36
37
38

39
40
41
42
43



44
45
46







-
+
-
-
-
-
-
+




-
+




-
-
-
+
+
+
* [[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|00001012051800]]
* [[Shape the list of zettel metadata with filter options|00001012051800]]
** [[Selection of zettel|00001012051810]]
** [[Zettel parts|00001012051820]]
** [[Limit the list length|00001012051830]]
** [[Content search|00001012051840]]
** [[Sort the list of zettel metadata|00001012052000]]
* [[Sort the list of zettel metadata|00001012052000]]
* [[List all tags|00001012052200]]
* [[List all roles|00001012052400]]

=== Working with zettel
* [[Create a new zettel|00001012053200]]
* 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|00001012054200]]
* [[Rename a zettel|00001012054400]]
* [[Delete a zettel|00001012054600]]
* Update metadata and content of a zettel
* Rename a zettel
* Delete a zettel

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

6
7
8
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: 00001012050200
title: API: Authenticate a client
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210712221945

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

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]] ''/v''[^The endpoint ''/a'' is already taken for the [[web user interface|00001014000000]], ""v"" stands for the German word ""verbürgen""] with a POST request:
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/v
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
```

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

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

In all cases, you will receive an JSON object will all [[relevant data|00001012921000]] to be used for further API calls.

**Important:** obtaining a token is a time-intensive process.
Zettelstore will delay every request to obtain a token for a certain amount of time.

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

6
7
8
9

10
11
12

13
14
15
16
17
18
19
20





-




-
+


-
+







id: 00001012050400
title: API: Renew an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210712222135

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]] ''/v'' and include the current access token in the ''Authorization'' header:
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/v
# curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456}
```
You may receive a new access token, or the current one if it was obtained not a long time ago.
However, the lifetime of the returned [[access token|00001012921000]] is accurate.

=== HTTP Status codes
; ''200''

Changes to docs/manual/00001012051800.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
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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: 00001012051800
title: API: Shape the list of zettel metadata
title: API: Shape the list of zettel metadata with filter options
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210721120658
modified: 20210510150129

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.
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.
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).
* [[Select|00001012051810]] just some zettel, based on metadata.
* You can specify which [[parts of a zettel|00001012051820]] must be returned.
* Only a specific amount of zettel will be selected by specifying [[a length and/or an offset|00001012051830]].
* [[Searching for specific content|00001012051840]], not just the metadata, is another way of selecting some zettel.
* The resulting list can be [[sorted|00001012052000]] according to various criteria.

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?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:
```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"}},
...
```
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.

=== Output only specific parts of a zettel
If you are just interested in the zettel identifier, you should add the ""''_part''"" query parameter:
```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"}]}
```
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"}]}
```
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:
```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"}]}
```
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'
{"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.

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

; ""'':''"", ""''~''"" (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.
  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"".
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"".

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.

This parameter loosely resembles the search box of the web user interface.

Deleted docs/manual/00001012051810.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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: 00001012051810
title: API: Select zettel based on their metadata
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210721121038

Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key.
According to the [[type|00001006030000]] of a metadata key, zettel are possibly selected.
All [[supported|00001006020000]] metadata keys have a well-defined type.
User-defined keys have the type ''e'' (string, possibly empty).

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?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:
```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"}},
...
```

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

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.
This is in contrast to above rule that the metadata value must exist before a match is done.
For example ``curl 'http://localhost:23123/z?back=!&backward='`` returns all zettel that are reachable via other zettel, but also references these zettel.

Above example shows that all sub-expressions of a select specification must be true so that no zettel is rejected from the final list.

If you specify the query parameter ''_negate'', either with or without a value, the whole selection will be negated.
Because of the precondition described above, ``curl 'http://127.0.0.1:23123/z?url=!com'`` and ``curl 'http://127.0.0.1:23123/z?url=com&_negate'`` may produce different lists.
The first query produces a zettel list, where each zettel does have a ''url'' metadata value, which does not contain the characters ""com"".
The second query produces a zettel list, that excludes any zettel containing a ''url'' metadata value that contains the characters ""com""; this also includes all zettel that do not contain the metadata key ''url''.

Deleted docs/manual/00001012051820.zettel.

1
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: 00001012051820
title: API: Shape the list of zettel metadata by returning specific parts of a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210721120743

=== Basic usage
If you are just interested in the zettel identifier, you should add the [[''_part''|00001012920800]] query parameter:
```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"}]}
```

=== Combined usage with select options
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"}]}
```
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"}]}
```

Deleted docs/manual/00001012051830.zettel.

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



















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012051830
title: API: Shape the list of zettel metadata by limiting its length
role: manual
tags: #api #manual #zettelstore
syntax: zmk

=== Limit
By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements:
```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"}]}
```

=== Offset
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'
{"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]}
```

Deleted docs/manual/00001012051840.zettel.

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




































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012051840
title: API: Shape the list of zettel metadata by searching the content
role: manual
tags: #api #manual #zettelstore
syntax: zmk

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.

If the search string starts with the character ""''!''"", it will be removed and the query matches all zettel that **do 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"":

; ""'':''"", ""''~''"" (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.
  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"".
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"".

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.

This parameter loosely resembles the search box of the web user interface.

Changes to 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
23
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
modified: 20210709162242

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''|00001012051830]]"" to obtain just one random zettel.
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/00001012053200.zettel.

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



























































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210713163927

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

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

Other keys will be ignored.
Even these three keys are just optional.
The body of the HTTP POST request must not be empty and it must contain a JSON object.

Therefore, a body containing just ''{}'' is perfectly valid.
The new zettel will have no content, its title will be set to the value of [[''default-title''|00001004020000#default-title]] (default: ""Untitled""), its role is set to the value of [[''default-role''|00001004020000#default-role]] (default: ""zettel""), and its syntax is set to the value of [[''default-syntax''|00001004020000#default-syntax]] (default: ""zmk"").

```
# curl -X POST --data '{}' http://127.0.0.1:23123/z
{"id":"20210713161000","url":"/z/20210713161000"}
```
If creating the zettel was successful, the HTTP response will contain a JSON object with two keys:
; ''"id"''
: Contains the zettel identifier of the created zettel for further usage.
; ''"url"''
: The URL for [[reading metadata and content|00001012053400]] of the new zettel.
  In most cases, the URL is a relative one.
  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.

In addition, the HTTP response header contains a key ''Location'' with the same value of the relative URL.

As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing:
```
# curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' http://127.0.0.1:23123/z
{"id":"20210713163100","url":"/z/20210713163100"}
```
=== HTTP Status codes
; ''201''
: Zettel creation was successful, the body contains a JSON object that contains its zettel identifier.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, the JSON was not formed according to above rules.
; ''403''
: You are not allowed to create a new zettel.

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

3
4

5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

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


-


-
+







-
+







+


















+
+

-
+
+
+
+
+
+







id: 00001012053600
title: API: Retrieve references of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210702183357
role: manual

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.

````
# curl http://127.0.0.1:23123/l/00001012053600
{"id":"00001012053600","url":"/z/00001012053600","links":{"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"}]},"images":{}}
{"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",
        "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": {},
  "images": {
    "outgoing": [],
    "local": [],
    "external": []
  },
  "cites": []
}
````
=== Kind
The following to-level JSON keys are returned:
; ''id''
: The zettel identifier for which the references were requested.
; ''url''
73
74
75
76
77
78
79
80
81
82
83


84
85
86
87
88
89
90
80
81
82
83
84
85
86

87


88
89
90
91
92
93
94
95
96







-

-
-
+
+







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
| ''meta'' |32| external reference, stored in metadata
| ''zettel'' | (6)|incoming and outgoing references
| ''material'' |(56)| local and external references
| ''all'' | (62)| incoming, outgoing, local, and external references
| ''material'' |(24)| local and external references
| ''all'' | (30)| incoming, outgoing, local, and external references

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.

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.

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

6
7
8
9
10

11
12
13
14
15
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: 00001012053800
title: API: Retrieve context of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210712223623

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//:
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]] ''/x/{ID}''[^Mnemonic: conte**X**t].
To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/y/{ID}''.

````
# curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2'
# 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",

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





-







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

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.
26
27
28
29
30
31
32
33

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

32
33
34
35
36
37
38
39







-
+







{"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",
  "list": [
  "order": [
    {
      "id": "00001001000000",
      "url": "/z/00001001000000",
      "meta": {...}
    },
    {
      "id": "00001002000000",

Deleted 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

































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012054200
title: API: Update a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210713163606

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 (14 digits).
You must send a HTTP PUT request to that endpoint:

```
# curl -X PUT --data '{}' http://127.0.0.1:23123/z/00001012054200
```
This will put some empty content and metadata to the zettel you are currently reading.
As usual, some metadata will be calculated if it is empty.

The body of the HTTP response is empty, if the request was successful.

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

Deleted docs/manual/00001012054400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012054400
title: API: Rename a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210713163708

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, for example.

The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).
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.
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|00001012053400]] 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]] seven 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.

Deleted 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


























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012054600
title: API: Delete a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210713163722

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 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 (14 digits).
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.

Changes to docs/manual/00001012920000.zettel.

1
2
3
4
5
6

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







23
24
25
26
27
28




29
30
31
32
33
34
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17





18
19
20
21
22
23
24






25
26
27
28

29
30
31
32
33





-
+











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





id: 00001012920000
title: Endpoints used by the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20210712225257
modified: 20210511131339

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

The following letters are currently in use:

|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic
| ''l'' |  | GET: [[list references|00001012053600]] | **L**inks
| ''o'' |  | GET: [[list zettel order|00001012054000]] | **O**rder
| ''r'' | GET: [[list roles|00001012052400]] | | **R**oles
| ''t'' | GET: [[list tags|00001012052200]] || **T**ags
|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]]
| ''a'' | POST: [[Client authentication|00001012050200]] |
|       | 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]]
| ''v'' | POST: [[client authentication|00001012050200]] | | **V**erbürgen[^German translation for ""authentication"", since ''/a'' is already in use by the [[web user interface|00001014000000]].]
|       | PUT: [[renew access token|00001012050400]] |
| ''x'' |  | GET: [[list zettel context|00001012053800]] | Conte**x**t
| ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053400]] | **Z**ettel
|       | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]]
|       |  | DELETE: [[delete the zettel|00001012054600]]
| ''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
|       |  | MOVE: [[rename the zettel|00001012054400]]

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

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

Changes to domain/content.go.

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

18
19
20
21
22
23


24
25
26

27
28
29

30
31

32
33
34
35
36
37
38
39
40

41
42
43

44
45
46
47

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
1
2
3
4
5
6
7
8
9
10
11
12
13




14
15





16
17



18

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 (
	"encoding/base64"
	"errors"
	"unicode/utf8"
import "unicode/utf8"

	"zettelstore.de/z/strfun"
)

// Content is just the content of a zettel.
type Content struct {
// Content is just the uninterpreted content of a zettel.
type Content string
	data     string
	isBinary bool
}


// NewContent creates a new content from a string.
func NewContent(s string) Content {
func NewContent(s string) Content { return Content(s) }
	return Content{data: s, isBinary: calcIsBinary(s)}
}


// Set content to new string value.
func (zc *Content) Set(s string) {
	zc.data = s
	zc.isBinary = calcIsBinary(s)
}

// AsString returns the content itself is a string.
func (zc *Content) AsString() string { return zc.data }
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.data) }
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 { return zc.isBinary }
func (zc Content) IsBinary() bool {

// TrimSpace remove some space character in content, if it is not binary content.
func (zc *Content) TrimSpace() {
	if zc.isBinary {
		return
	}
	zc.data = strfun.TrimSpaceRight(zc.data)
}

// Encode content for future transmission.
func (zc *Content) Encode() (data, encoding string) {
	if !zc.isBinary {
		return zc.data, ""
	}
	return base64.StdEncoding.EncodeToString([]byte(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 = data
	case "base64":
		decoded, err := base64.StdEncoding.DecodeString(data)
		if err != nil {
			return err
		}
		zc.data = string(decoded)
	s := string(zc)
	default:
		return errors.New("unknown encoding " + encoding)
	}
	zc.isBinary = calcIsBinary(zc.data)
	return nil
}

func calcIsBinary(s string) bool {
	if !utf8.ValidString(s) {
		return true
	}
	l := len(s)
	for i := 0; i < l; i++ {
		if s[i] == 0 {
			return true
		}
	}
	return false
}

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

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

package domain_test

import (
	"testing"

	"zettelstore.de/z/domain"
)

func TestContentIsBinary(t *testing.T) {
	t.Parallel()
	td := []struct {
		s   string
		exp bool
	}{
		{"abc", false},
		{"äöü", false},
		{"", false},

Changes to domain/id/id.go.

21
22
23
24
25
26
27
28

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

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
21
22
23
24
25
26
27

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

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

61
62
63
64
65
66
67







-
+











-
+




















-







// 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 box. They are mentioned there literally, because these
//       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)
	BoxManagerZid           = Zid(20)
	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)
	UserCSSZid = Zid(25001)

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

Changes to domain/id/id_test.go.

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

21
22
23
24
25
26
27







-







import (
	"testing"

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

func TestIsValid(t *testing.T) {
	t.Parallel()
	validIDs := []string{
		"00000000000001",
		"00000000000020",
		"00000000000300",
		"00000000004000",
		"00000000050000",
		"00000000600000",

Changes to domain/id/set_test.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
15
16
17
18
19
20
21

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

39
40
41
42
43
44
45







-

















-







import (
	"testing"

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

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 TestSetIntersection(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},
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
59
60
61
62
63
64
65

66
67
68
69
70
71
72







-







		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) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},

Changes to domain/id/slice_test.go.

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

22
23
24
25
26
27
28
29
30

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

44
45
46
47
48
49
50







-









-













-







import (
	"testing"

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

func TestSliceSort(t *testing.T) {
	t.Parallel()
	zs := id.Slice{9, 4, 6, 1, 7}
	zs.Sort()
	exp := id.Slice{1, 4, 6, 7, 9}
	if !zs.Equal(exp) {
		t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs)
	}
}

func TestCopy(t *testing.T) {
	t.Parallel()
	var orig id.Slice
	got := orig.Copy()
	if got != nil {
		t.Errorf("Nil copy resulted in %v", got)
	}
	orig = id.Slice{9, 4, 6, 1, 7}
	got = orig.Copy()
	if !orig.Equal(got) {
		t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got)
	}
}

func TestSliceEqual(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Slice
		exp    bool
	}{
		{nil, nil, true},
		{nil, id.Slice{}, true},
		{nil, id.Slice{1}, false},
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
62
63
64
65
66
67
68

69
70
71
72
73
74
75







-







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

Changes to domain/meta/meta.go.

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27







-
+








import (
	"regexp"
	"sort"
	"strings"

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

type keyUsage int

const (
	_             keyUsage = iota
	usageUser              // Key will be manipulated by the user
85
86
87
88
89
90
91
92

93
94
95
96
97
98
99
85
86
87
88
89
90
91

92
93
94
95
96
97
98
99







-
+







}

// 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)}
	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)
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

137
138
139
140
141
142
143
144
145
146
147
148
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
111
112
113
114
115
116
117

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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







-


















+















-
-
-












-



-







	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, "")
	KeyBoxNumber         = registerKey("box-number", TypeNumber, usageComputed, "")
	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, "")
)

// NewPrefix is the prefix for metadata key in template zettel for creating new zettel.
const NewPrefix = "new-"

// 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"
	ValueUserRoleCreator   = "creator"
	ValueUserRoleReader    = "reader"
	ValueUserRoleWriter    = "writer"
	ValueUserRoleOwner     = "owner"
	ValueVisibilityCreator = "creator"
	ValueVisibilityExpert  = "expert"
	ValueVisibilityOwner   = "owner"
	ValueVisibilityLogin   = "login"
	ValueVisibilityPublic  = "public"
)

// Meta contains all meta-data of a zettel.
232
233
234
235
236
237
238
239

240
241
242
243
244
245
246
227
228
229
230
231
232
233

234
235
236
237
238
239
240
241







-
+







func (m *Meta) Set(key, value string) {
	if key != KeyID {
		m.pairs[key] = trimValue(value)
	}
}

func trimValue(value string) string {
	return strings.TrimFunc(value, input.IsSpace)
	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

Changes to domain/meta/meta_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
17
18
19
20
21
22
23

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

39
40
41
42
43
44
45







-















-








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

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	t.Parallel()
	validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)}
	for _, key := range validKeys {
		if !KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as invalid key", key)
		}
	}
	invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)}
	for _, key := range invalidKeys {
		if KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as valid key", key)
		}
	}
}

func TestTitleHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(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)
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
78
79
80
81
82
83
84

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

102
103
104
105
106
107
108







-

















-







	}
	if len(exp) < len(got) {
		t.Errorf("Extra tags: %q", got[len(exp):])
	}
}

func TestTagsHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	checkSet(t, []string{}, m, 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) {
	t.Parallel()
	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)
139
140
141
142
143
144
145
146
147
148
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
135
136
137
138
139
140
141

142
143
144
145
146
147
148
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







-



















-
















-







				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(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) {
	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},

Changes to domain/meta/parse.go.

13
14
15
16
17
18
19

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







+








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()
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83

84
85
86
87
88
89
90
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83

84
85
86
87
88
89
90
91







-
+








-
+







	var val string
	for {
		skipSpace(inp)
		pos = inp.Pos
		skipToEOL(inp)
		val += inp.Src[pos:inp.Pos]
		inp.EatEOL()
		if !input.IsSpace(inp.Ch) {
		if !runes.IsSpace(inp.Ch) {
			break
		}
		val += " "
	}
	addToMeta(m, key, val)
}

func skipSpace(inp *input.Input) {
	for input.IsSpace(inp.Ch) {
	for runes.IsSpace(inp.Ch) {
		inp.Next()
	}
}

func skipToEOL(inp *input.Input) {
	for {
		switch inp.Ch {
119
120
121
122
123
124
125
126

127
128
129
130
131
132
133
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134







-
+







	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 rejected elements.
		// 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)

Changes to domain/meta/parse_test.go.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41
42







-










-







)

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput(src))
}

func TestEmpty(t *testing.T) {
	t.Parallel()
	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) {
	t.Parallel()
	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"},
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
57
58
59
60
61
62
63

64
65
66
67
68
69
70







-







	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) {
	t.Parallel()
	testcases := []struct {
		input string
		exp   []meta.Pair
	}{
		{"", []meta.Pair{}},
		{" a:b", []meta.Pair{{"a", "b"}}},
		{"%a:b", []meta.Pair{}},
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
105
106
107
108
109
110
111

112
113
114
115
116
117
118







-







			return false
		}
	}
	return true
}

func TestPrecursorIDSet(t *testing.T) {
	t.Parallel()
	var testdata = []struct {
		inp string
		exp string
	}{
		{"", ""},
		{"123", ""},
		{"12345678901234", "12345678901234"},

Changes to domain/meta/type.go.

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

17
18
19
20
21
22
23







-








// Package meta provides the domain specific type 'meta'.
package meta

import (
	"strconv"
	"strings"
	"sync"
	"time"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
46
47
48
49
50
51
52

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

98
99
100
101
102
103
104
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63












64
65
66
67
68
69
















70
71
72
73
74
75
76
77







+











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






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







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

var (
	cachedTypedKeys = make(map[string]*DescriptionType)
	mxTypedKey      sync.RWMutex
)

func typedKey(key string, t *DescriptionType) *DescriptionType {
	mxTypedKey.Lock()
	defer mxTypedKey.Unlock()
	cachedTypedKeys[key] = t
	return t
}

// 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
	}
	mxTypedKey.RLock()
	k, ok := cachedTypedKeys[key]
	mxTypedKey.RUnlock()
	if ok {
		return k
	}
	if strings.HasSuffix(key, "-url") {
		return typedKey(key, TypeURL)
	}
	if strings.HasSuffix(key, "-number") {
		return typedKey(key, TypeNumber)
	}
	if strings.HasSuffix(key, "-zid") {
		return typedKey(key, TypeID)
	}
	return TypeEmpty
	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)

Changes to domain/meta/type_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
17
18
19
20
21
22
23

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

42
43
44
45
46
47
48







-


















-







	"time"

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

func TestNow(t *testing.T) {
	t.Parallel()
	m := meta.New(id.Invalid)
	m.SetNow("key")
	val, ok := m.Get("key")
	if !ok {
		t.Error("Unable to get value of key")
	}
	if len(val) != 14 {
		t.Errorf("Value is not 14 digits long: %q", val)
	}
	if _, err := strconv.ParseInt(val, 10, 64); err != nil {
		t.Errorf("Unable to parse %q as an int64: %v", val, err)
	}
	if _, ok := m.GetTime("key"); !ok {
		t.Errorf("Unable to get time from value %q", val)
	}
}

func TestGetTime(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		value string
		valid bool
		exp   time.Time
	}{
		{"", false, time.Time{}},
		{"1", false, time.Time{}},

Changes to domain/meta/values.go.

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

30
31
32
33



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61



62
63
64
65
66
67
68
69
70
15
16
17
18
19
20
21

22
23
24
25
26
27

28




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

49
50
51
52
53
54




55
56
57
58
59
60
61
62
63
64
65
66







-






-
+
-
-
-
-
+
+
+

















-






-
-
-
-
+
+
+









type Visibility int

// Supported values for visibility.
const (
	_ Visibility = iota
	VisibilityUnknown
	VisibilityPublic
	VisibilityCreator
	VisibilityLogin
	VisibilityOwner
	VisibilityExpert
)

var visMap = map[string]Visibility{
	ValueVisibilityPublic:  VisibilityPublic,
	ValueVisibilityPublic: VisibilityPublic,
	ValueVisibilityCreator: VisibilityCreator,
	ValueVisibilityLogin:   VisibilityLogin,
	ValueVisibilityOwner:   VisibilityOwner,
	ValueVisibilityExpert:  VisibilityExpert,
	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
	UserRoleCreator
	UserRoleReader
	UserRoleWriter
	UserRoleOwner
)

var urMap = map[string]UserRole{
	ValueUserRoleCreator: UserRoleCreator,
	ValueUserRoleReader:  UserRoleReader,
	ValueUserRoleWriter:  UserRoleWriter,
	ValueUserRoleOwner:   UserRoleOwner,
	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
}

Changes to domain/meta/write_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
40
41
42
43
44
45
46

47
48
49
50
51
52
53
54
55
56







-










	m.Write(&sb, true)
	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")
}

Changes to encoder/buffer.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
47












48
49

50
51

52
53
54
55
56
57
58
59
60
61
62
63
28
29
30
31
32
33
34

35












36
37
38
39
40
41
42
43
44
45
46
47
48

49


50





51
52
53
54
55
56
57







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

-
+
-
-
+
-
-
-
-
-







// 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 {
	if w.err == nil {
		return 0, w.err
	}
	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
}

		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) {
func (w *BufWriter) WriteString(s string) (int, error) {
	if w.err != nil {
		return
	return w.Write([]byte(s))
	}
	w.buf = append(w.buf, s...)
	if len(w.buf) > 2048 {
		w.flush()
	}
}

// WriteStrings writes the contents of sl into the buffer.
func (w *BufWriter) WriteStrings(sl ...string) {
	for _, s := range sl {
		w.WriteString(s)
	}

Changes to encoder/encfun/encfun.go.

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

32
33
34
35
36
37
38
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37







-













-
+








// Package encfun provides some helper function to work with encodings.
package encfun

import (
	"strings"

	"zettelstore.de/z/api"
	"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(api.EncoderText, nil)
	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.

12
13
14
15
16
17
18

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

21
22
23
24
25
26
27







+

-







// tree into some text form.
package encoder

import (
	"errors"
	"io"
	"log"
	"sort"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/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)
37
38
39
40
41
42
43
44

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


59
60
61

62
63
64
65
66

67
68
69
70
71
72
73
74
75
76



77
78
79

80
81
82
83
84
85


86
87
88
89


90
91
92

93
37
38
39
40
41
42
43

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


57
58
59
60

61
62
63
64
65

66
67
68
69
70
71
72
73



74
75
76
77
78
79
80
81
82
83
84


85
86
87
88


89
90
91
92

93
94







-
+












-
-
+
+


-
+




-
+







-
-
-
+
+
+



+




-
-
+
+


-
-
+
+


-
+

	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 api.EncodingEnum, env *Environment) Encoder {
func Create(format string, env *Environment) Encoder {
	if info, ok := registry[format]; ok {
		return info.Create(env)
	}
	return nil
}

// Info stores some data about an encoder.
type Info struct {
	Create  func(*Environment) Encoder
	Default bool
}

var registry = map[api.EncodingEnum]Info{}
var defFormat api.EncodingEnum
var registry = map[string]Info{}
var defFormat string

// Register the encoder for later retrieval.
func Register(format api.EncodingEnum, info Info) {
func Register(format string, info Info) {
	if _, ok := registry[format]; ok {
		log.Fatalf("Writer with format %q already registered", format)
	}
	if info.Default {
		if defFormat != api.EncoderUnknown && defFormat != format {
		if defFormat != "" && defFormat != format {
			log.Fatalf("Default format already set: %q, new format: %q", defFormat, format)
		}
		defFormat = format
	}
	registry[format] = info
}

// GetFormats returns all registered formats, ordered by format code.
func GetFormats() []api.EncodingEnum {
	result := make([]api.EncodingEnum, 0, len(registry))
// 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)
	}
	sort.Strings(result)
	return result
}

// GetDefaultFormat returns the format that should be used as default.
func GetDefaultFormat() api.EncodingEnum {
	if defFormat != api.EncoderUnknown {
func GetDefaultFormat() string {
	if defFormat != "" {
		return defFormat
	}
	if _, ok := registry[api.EncoderJSON]; ok {
		return api.EncoderJSON
	if _, ok := registry["json"]; ok {
		return "json"
	}
	log.Fatalf("No default format given")
	return api.EncoderUnknown
	return ""
}

Changes to encoder/htmlenc/block.go.

15
16
17
18
19
20
21








22
23


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


30
31
32
33
34
35
36
37
38







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







	"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.Kind {
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)
49
50
51
52
53
54
55
56

57
58
59
60
61
62
63
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71







-
+







	case ast.VerbatimHTML:
		for _, line := range vn.Lines {
			if !ignoreHTMLText(line) {
				v.b.WriteStrings(line, "\n")
			}
		}
	default:
		panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind))
		panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
	}
}

var htmlSnippetsIgnore = []string{
	"<script",
	"</script",
	"<iframe",
91
92
93
94
95
96
97

98

99
100
101
102

103
104
105
106
107
108
109
110
111
112

113
114
115
116
117
118
119
120
121

122
123
124

125
126
127
128
129
130

131

132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

147
148
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
99
100
101
102
103
104
105
106

107
108
109
110

111
112
113
114
115
116
117
118
119
120

121
122
123
124
125
126
127
128
129

130
131
132

133
134
135
136
137
138
139
140

141
142
143
144
145
146
147
148
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







+
-
+



-
+









-
+








-
+


-
+






+
-
+














-
+



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




+
-
+



-
+





-
+

-
+







			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) {
func (v *visitor) VisitRegion(rn *ast.RegionNode) {
	var code string
	attrs := rn.Attrs
	oldVerse := v.inVerse
	switch rn.Kind {
	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 kind %v", rn.Kind))
		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")
	ast.WalkBlockSlice(v, rn.Blocks)
	v.acceptBlockSlice(rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.b.WriteString("<cite>")
		ast.WalkInlineSlice(v, rn.Inlines)
		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) {
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('>')
	ast.WalkInlineSlice(v, hn.Inlines)
	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 mapNestedListKind = map[ast.NestedListKind]string{
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) {
func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
	v.lang.push(ln.Attrs)
	defer v.lang.pop()

	if ln.Kind == ast.NestedListQuote {
	if ln.Code == ast.NestedListQuote {
		// NestedListQuote -> HTML <blockquote> doesn't use <li>...</li>
		v.writeQuotationList(ln)
		return
	}

	code, ok := mapNestedListKind[ln.Kind]
	code, ok := listCode[ln.Code]
	if !ok {
		panic(fmt.Sprintf("Invalid list kind %v", ln.Kind))
		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 {
186
187
188
189
190
191
192
193

194
195
196
197
198
199

200
201
202
203
204
205
206
208
209
210
211
212
213
214

215
216
217
218
219
220

221
222
223
224
225
226
227
228







-
+





-
+







		if pn := getParaItem(item); pn != nil {
			if inPara {
				v.b.WriteByte('\n')
			} else {
				v.b.WriteString("<p>")
				inPara = true
			}
			ast.WalkInlineSlice(v, pn.Inlines)
			v.acceptInlineSlice(pn.Inlines)
		} else {
			if inPara {
				v.writeEndPara()
				inPara = false
			}
			ast.WalkItemSlice(v, item)
			v.acceptItemSlice(item)
		}
	}
	if inPara {
		v.writeEndPara()
	}
	v.b.WriteString("</blockquote>\n")
}
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
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







-
+



-
+





-
+



-
-
-
-
+
+
+
+
+
+
+



-
+











+
-
+








// 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 {
			ast.WalkInlineSlice(v, para.Inlines)
			v.acceptInlineSlice(para.Inlines)
			return
		}
	}
	ast.WalkItemSlice(v, ins)
	v.acceptItemSlice(ins)
}

func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) {
	if len(ds) == 1 {
		if para, ok := ds[0].(*ast.ParaNode); ok {
			ast.WalkInlineSlice(v, para.Inlines)
			v.acceptInlineSlice(para.Inlines)
			return
		}
	}
	ast.WalkDescriptionSlice(v, ds)
}

func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	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>")
		ast.WalkInlineSlice(v, descr.Term)
		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) {
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 {
306
307
308
309
310
311
312
313

314
315
316
317
318
319

320

321
322
323
324
325
326
327
332
333
334
335
336
337
338

339
340
341
342
343
344
345
346

347
348
349
350
351
352
353
354







-
+






+
-
+







	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])
			ast.WalkInlineSlice(v, cell.Inlines)
			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) {
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")

Changes to encoder/htmlenc/htmlenc.go.

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

28
29
30
31
32
33
34
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33







-








-
+







// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"io"
	"strings"

	"zettelstore.de/z/api"
	"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(api.EncoderHTML, encoder.Info{
	encoder.Register("html", encoder.Info{
		Create: func(env *encoder.Environment) encoder.Encoder { return &htmlEncoder{env: env} },
	})
}

type htmlEncoder struct {
	env *encoder.Environment
}
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

90
91
92
93
94
95
96
97
98
99
100
101

102
103
104
47
48
49
50
51
52
53

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

67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

88
89
90
91
92
93
94
95
96
97
98
99

100
101
102
103







-
+












-
+




















-
+











-
+



	v.b.WriteStrings("<title>", encfun.MetaAsText(zn.InhMeta, meta.KeyTitle), "</title>")
	if inhMeta {
		v.acceptMeta(zn.InhMeta)
	} else {
		v.acceptMeta(zn.Meta)
	}
	v.b.WriteString("\n</head>\n<body>\n")
	ast.WalkBlockSlice(v, zn.Ast)
	v.acceptBlockSlice(zn.Ast)
	v.writeEndnotes()
	v.b.WriteString("</body>\n</html>")
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as HTML5.
func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	v := newVisitor(he, w)

	// Write title
	if title, ok := m.Get(meta.KeyTitle); ok {
		textEnc := encoder.Create(api.EncoderText, nil)
		textEnc := encoder.Create("text", nil)
		var sb strings.Builder
		textEnc.WriteInlines(&sb, parser.ParseMetadata(title))
		v.b.WriteStrings("<meta name=\"zs-", meta.KeyTitle, "\" content=\"")
		v.writeQuotedEscaped(sb.String())
		v.b.WriteString("\">")
	}

	// Write other metadata
	v.acceptMeta(m)
	length, err := v.b.Flush()
	return length, err
}

func (he *htmlEncoder) 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)
	ast.WalkBlockSlice(v, bs)
	v.acceptBlockSlice(bs)
	v.writeEndnotes()
	length, err := v.b.Flush()
	return length, 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 {
		v.inInteractive = env.Interactive
	}
	ast.WalkInlineSlice(v, is)
	v.acceptInlineSlice(is)
	length, err := v.b.Flush()
	return length, err
}

Changes to encoder/htmlenc/inline.go.

16
17
18
19
20
21
22























23

24
25
26
27
28
29
30
31
32
33
34

35

36
37
38

39
40
41
42
43
44
45
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

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

59
60
61

62
63
64
65
66
67
68
69







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











+
-
+


-
+







	"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) {
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) {
func (v *visitor) VisitLink(ln *ast.LinkNode) {
	ln, n := v.env.AdaptLink(ln)
	if n != nil {
		ast.Walk(v, n)
		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:
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

91
92
93
94

95

96
97
98

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

120
121
122
123
124
125
126
127
128

129

130
131
132

133
134
135
136
137
138
139
140
141
142
143

144
145
146

147

148
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
90
91
92
93
94
95
96

97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

114
115
116
117
118
119

120
121
122

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143

144
145
146
147
148
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







-
+
















-
+




+
-
+


-
+




















-
+









+
-
+


-
+










-
+



+
-
+











+
-
+








+
-
+





-
+







		}
		v.b.WriteString("<a href=\"")
		v.writeQuotedEscaped(ln.Ref.Value)
		v.b.WriteByte('"')
		v.visitAttributes(ln.Attrs)
		v.b.WriteByte('>')
		v.inInteractive = true
		ast.WalkInlineSlice(v, ln.Inlines)
		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
	ast.WalkInlineSlice(v, ins)
	v.acceptInlineSlice(ins)
	v.inInteractive = false
	v.b.WriteString("</a>")
}

// VisitImage writes HTML code for images.
func (v *visitor) visitImage(in *ast.ImageNode) {
func (v *visitor) VisitImage(in *ast.ImageNode) {
	in, n := v.env.AdaptImage(in)
	if n != nil {
		ast.Walk(v, n)
		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=\"")
	ast.WalkInlineSlice(v, in.Inlines)
	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) {
func (v *visitor) VisitCite(cn *ast.CiteNode) {
	cn, n := v.env.AdaptCite(cn)
	if n != nil {
		ast.Walk(v, n)
		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(", ")
		ast.WalkInlineSlice(v, cn.Inlines)
		v.acceptInlineSlice(cn.Inlines)
	}
}

// VisitFootnote write HTML code for a footnote.
func (v *visitor) visitFootnote(fn *ast.FootnoteNode) {
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) {
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) {
func (v *visitor) VisitFormat(fn *ast.FormatNode) {
	v.lang.push(fn.Attrs)
	defer v.lang.pop()

	var code string
	attrs := fn.Attrs
	switch fn.Kind {
	switch fn.Code {
	case ast.FormatItalic:
		code = "i"
	case ast.FormatEmph:
		code = "em"
	case ast.FormatBold:
		code = "b"
	case ast.FormatStrong:
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
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







-
+




-
+







-
+







	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 kind %v", fn.Kind))
		panic(fmt.Sprintf("Unknown format code %v", fn.Code))
	}
	v.b.WriteStrings("<", code)
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	ast.WalkInlineSlice(v, fn.Inlines)
	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('>')
	ast.WalkInlineSlice(v, ins)
	v.acceptInlineSlice(ins)
	v.b.WriteString("</span>")

}

var langQuotes = map[string][2]string{
	meta.ValueLangEN: {"&ldquo;", "&rdquo;"},
	"de":             {"&bdquo;", "&ldquo;"},
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
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







-
+






+
-
-
+
+















-
+







	if withSpan {
		v.b.WriteString("<span")
		v.visitAttributes(fn.Attrs)
		v.b.WriteByte('>')
	}
	openingQ, closingQ := getQuotes(v.lang.top())
	v.b.WriteString(openingQ)
	ast.WalkInlineSlice(v, fn.Inlines)
	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.Kind {
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 kind %v", ln.Kind))
		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()

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

2
3
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 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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) {
	t.Parallel()
	exp := "de"
	s := newLangStack(exp)
	if got := s.top(); got != exp {
		t.Errorf("Init: expected %q, but got %q", exp, got)
		return
	}

Changes to encoder/htmlenc/visitor.go.

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
41
42
43
44
45
46
47































































48
49
50
51
52
53
54







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







	return &visitor{
		env:  he.env,
		b:    encoder.NewBufWriter(w),
		lang: newLangStack(lang),
	}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.ParaNode:
		v.b.WriteString("<p>")
		ast.WalkInlineSlice(v, n.Inlines)
		v.writeEndPara()
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.b.WriteString("<hr")
		v.visitAttributes(n.Attrs)
		if v.env.IsXHTML() {
			v.b.WriteString(" />\n")
		} else {
			v.b.WriteString(">\n")
		}
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.writeHTMLEscaped(n.Text)
	case *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(n.Tag)
		v.b.WriteString("</span>")
	case *ast.SpaceNode:
		if v.inVerse || v.env.IsXHTML() {
			v.b.WriteString(n.Lexeme)
		} else {
			v.b.WriteByte(' ')
		}
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.ImageNode:
		v.visitImage(n)
	case *ast.CiteNode:
		v.visitCite(n)
	case *ast.FootnoteNode:
		v.visitFootnote(n)
	case *ast.MarkNode:
		v.visitMark(n)
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

var mapMetaKey = map[string]string{
	meta.KeyCopyright: "copyright",
	meta.KeyLicense:   "license",
}

func (v *visitor) acceptMeta(m *meta.Meta) {
	for _, pair := range m.Pairs(true) {
143
144
145
146
147
148
149
















150
151
152
153
154
155
156
157
158
159
160
161

162
163
164
165
166
167
168
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

114
115
116
117
118
119
120
121







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











-
+







}

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\">")
			ast.WalkInlineSlice(v, fn.Inlines)
			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")
	}

Changes to encoder/jsonenc/djsonenc.go.

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

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

42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58
59

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

74
75
76
77
78
79
80
81

82
83
84
85
86
87
88
89
90
91
92
93
94
95

96

97
98
99
100
101



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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
13
14
15
16
17
18
19

20
21
22
23
24
25
26

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

40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
55
56
57

58
59
60
61
62
63
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

96





97
98
99

































































































100

101
102

103
104
105
106
107
108
109


110
111
112

113
114

115
116
117
118
119

120
121
122
123

124
125
126

127
128
129
130
131
132
133


134
135
136

137
138

139
140
141

142
143
144

145
146
147
148
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







-







-
+












-
+







-
+









-
+













-
+







-
+














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

-


-
+





+
-
-
+
+

-
+

-
+



+
-
+
+


-
+


-
+





+
-
-
+
+

-
+

-
+


-
+


-
+

+


+
-
+









-
+
+


+
+
+
+
+
+
+
-
+





+
-
-
+
+


-
-
+
+
-
-
-

-
+

-
+


+
-
+

-
-
-
+
+
+
+
+

-
+



-
-
-
-
-
-
+
+




-
+


+
-
+






+
-
+
+







+
-
+
+


+
-
+
+




-
+











-
+


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












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


-
+








import (
	"fmt"
	"io"
	"sort"
	"strconv"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/encfun"
)

func init() {
	encoder.Register(api.EncoderDJSON, encoder.Info{
	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.walkInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle))
	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.walkBlockSlice(zn.Ast)
	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.walkInlineSlice(encfun.MetaAsInlineSlice(m, meta.KeyTitle))
	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.walkBlockSlice(bs)
	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.walkInlineSlice(is)
	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) Visit(node ast.Node) ast.Visitor {
func (v *detailVisitor) VisitPara(pn *ast.ParaNode) {
	switch n := node.(type) {
	case *ast.ParaNode:
		v.writeNodeStart("Para")
		v.writeContentStart('i')
		v.walkInlineSlice(n.Inlines)
	v.writeNodeStart("Para")
	v.writeContentStart('i')
	v.acceptInlineSlice(pn.Inlines)
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.writeNodeStart("Hrule")
		v.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.BLOBNode:
		v.writeNodeStart("Blob")
		v.writeContentStart('q')
		writeEscaped(&v.b, n.Title)
		v.writeContentStart('s')
		writeEscaped(&v.b, n.Syntax)
		v.writeContentStart('o')
		v.b.WriteBase64(n.Blob)
		v.b.WriteByte('"')
	case *ast.TextNode:
		v.writeNodeStart("Text")
		v.writeContentStart('s')
		writeEscaped(&v.b, n.Text)
	case *ast.TagNode:
		v.writeNodeStart("Tag")
		v.writeContentStart('s')
		writeEscaped(&v.b, n.Tag)
	case *ast.SpaceNode:
		v.writeNodeStart("Space")
		if l := len(n.Lexeme); l > 1 {
			v.writeContentStart('n')
			v.b.WriteString(strconv.Itoa(l))
		}
	case *ast.BreakNode:
		if n.Hard {
			v.writeNodeStart("Hard")
		} else {
			v.writeNodeStart("Soft")
		}
	case *ast.LinkNode:
		n, n2 := v.env.AdaptLink(n)
		if n2 != nil {
			ast.Walk(v, n2)
			return nil
		}
		v.writeNodeStart("Link")
		v.visitAttributes(n.Attrs)
		v.writeContentStart('q')
		writeEscaped(&v.b, mapRefState[n.Ref.State])
		v.writeContentStart('s')
		writeEscaped(&v.b, n.Ref.String())
		v.writeContentStart('i')
		v.walkInlineSlice(n.Inlines)
	case *ast.ImageNode:
		v.visitImage(n)
	case *ast.CiteNode:
		v.writeNodeStart("Cite")
		v.visitAttributes(n.Attrs)
		v.writeContentStart('s')
		writeEscaped(&v.b, n.Key)
		if len(n.Inlines) > 0 {
			v.writeContentStart('i')
			v.walkInlineSlice(n.Inlines)
		}
	case *ast.FootnoteNode:
		v.writeNodeStart("Footnote")
		v.visitAttributes(n.Attrs)
		v.writeContentStart('i')
		v.walkInlineSlice(n.Inlines)
	case *ast.MarkNode:
		v.writeNodeStart("Mark")
		if len(n.Text) > 0 {
			v.writeContentStart('s')
			writeEscaped(&v.b, n.Text)
		}
	case *ast.FormatNode:
		v.writeNodeStart(mapFormatKind[n.Kind])
		v.visitAttributes(n.Attrs)
		v.writeContentStart('i')
		v.walkInlineSlice(n.Inlines)
	case *ast.LiteralNode:
		kind, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown literal kind %v", n.Kind))
		}
		v.writeNodeStart(kind)
		v.visitAttributes(n.Attrs)
		v.writeContentStart('s')
		writeEscaped(&v.b, n.Text)
	default:
		return v
	}
	v.b.WriteByte('}')
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
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) {
	kind, ok := mapVerbatimKind[vn.Kind]
func (v *detailVisitor) VisitVerbatim(vn *ast.VerbatimNode) {
	code, ok := verbatimCode[vn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind))
		panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
	}
	v.writeNodeStart(kind)
	v.writeNodeStart(code)
	v.visitAttributes(vn.Attrs)
	v.writeContentStart('l')
	for i, line := range vn.Lines {
		if i > 0 {
		v.writeComma(i)
			v.b.WriteByte(',')
		}
		writeEscaped(&v.b, line)
	}
	v.b.WriteByte(']')
	v.b.WriteString("]}")
}

var mapRegionKind = map[ast.RegionKind]string{
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) {
	kind, ok := mapRegionKind[rn.Kind]
func (v *detailVisitor) VisitRegion(rn *ast.RegionNode) {
	code, ok := regionCode[rn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %v", rn.Kind))
		panic(fmt.Sprintf("Unknown region code %v", rn.Code))
	}
	v.writeNodeStart(kind)
	v.writeNodeStart(code)
	v.visitAttributes(rn.Attrs)
	v.writeContentStart('b')
	v.walkBlockSlice(rn.Blocks)
	v.acceptBlockSlice(rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.writeContentStart('i')
		v.walkInlineSlice(rn.Inlines)
		v.acceptInlineSlice(rn.Inlines)
	}
	v.b.WriteByte('}')
}

// VisitHeading writes the JSON code for a heading.
func (v *detailVisitor) visitHeading(hn *ast.HeadingNode) {
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.walkInlineSlice(hn.Inlines)
	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 mapNestedListKind = map[ast.NestedListKind]string{
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(mapNestedListKind[ln.Kind])
func (v *detailVisitor) VisitNestedList(ln *ast.NestedListNode) {
	v.writeNodeStart(listCode[ln.Code])
	v.writeContentStart('c')
	for i, item := range ln.Items {
		v.writeComma(i)
		v.b.WriteByte('[')
		if i > 0 {
			v.b.WriteByte(',')
		for j, in := range item {
			v.writeComma(j)
			ast.Walk(v, in)
		}
		v.b.WriteByte(']')
		v.acceptItemSlice(item)
	}
	v.b.WriteByte(']')
	v.b.WriteString("]}")
}

// VisitDescriptionList emits a JSON description list.
func (v *detailVisitor) visitDescriptionList(dn *ast.DescriptionListNode) {
func (v *detailVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	v.writeNodeStart("DescriptionList")
	v.writeContentStart('g')
	for i, def := range dn.Descriptions {
		v.writeComma(i)
	v.writeContentStart('g')
	for i, def := range dn.Descriptions {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.b.WriteByte('[')
		v.walkInlineSlice(def.Term)
		v.acceptInlineSlice(def.Term)

		if len(def.Descriptions) > 0 {
			for _, b := range def.Descriptions {
				v.b.WriteString(",[")
				for j, dn := range b {
					v.writeComma(j)
					ast.Walk(v, dn)
				}
				v.b.WriteByte(']')
				v.b.WriteByte(',')
				v.acceptDescriptionSlice(b)
			}
		}
		v.b.WriteByte(']')
	}
	v.b.WriteByte(']')
	v.b.WriteString("]}")
}

// VisitTable emits a JSON table.
func (v *detailVisitor) visitTable(tn *ast.TableNode) {
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.writeComma(i)
			v.b.WriteByte(',')
		}
		v.writeCell(cell)
	}
	v.b.WriteString("],")

	// Table rows
	v.b.WriteByte('[')
	for i, row := range tn.Rows {
		if i > 0 {
		v.writeComma(i)
			v.b.WriteByte(',')
		}
		v.b.WriteByte('[')
		for j, cell := range row {
			if j > 0 {
			v.writeComma(j)
				v.b.WriteByte(',')
			}
			v.writeCell(cell)
		}
		v.b.WriteByte(']')
	}
	v.b.WriteString("]]")
	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.walkInlineSlice(cell.Inlines)
	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) {
func (v *detailVisitor) VisitImage(in *ast.ImageNode) {
	in, n := v.env.AdaptImage(in)
	if n != nil {
		ast.Walk(v, n)
		n.Accept(v)
		return
	}
	v.writeNodeStart("Image")
	v.visitAttributes(in.Attrs)
	if in.Ref == nil {
		v.writeContentStart('j')
		v.b.WriteString("\"s\":")
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
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







-
+

+


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

















+
+
+
+
+
+
+
+
+
-
+







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


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




-
+


+
-
-
+
+
+







		v.b.WriteByte('}')
	} else {
		v.writeContentStart('s')
		writeEscaped(&v.b, in.Ref.String())
	}
	if len(in.Inlines) > 0 {
		v.writeContentStart('i')
		v.walkInlineSlice(in.Inlines)
		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 mapFormatKind = map[ast.FormatKind]string{
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 mapLiteralKind = map[ast.LiteralKind]string{
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) walkBlockSlice(bns ast.BlockSlice) {
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(']')
}
		v.writeComma(i)
		ast.Walk(v, bn)

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) walkInlineSlice(ins ast.InlineSlice) {
func (v *detailVisitor) acceptInlineSlice(ins ast.InlineSlice) {
	v.b.WriteByte('[')
	for i, in := range ins {
		if i > 0 {
		v.writeComma(i)
		ast.Walk(v, in)
			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 {
493
494
495
496
497
498
499

500


501
502
503
504
505
506
507
508
509
510
511
512
559
560
561
562
563
564
565
566

567
568
569
570
571
572
573
574













+
-
+
+






-
-
-
-
-
-
		}
	}
}

func (v *detailVisitor) writeSetValue(value string) {
	v.b.WriteByte('[')
	for i, val := range meta.ListFromValue(value) {
		if i > 0 {
		v.writeComma(i)
			v.b.WriteByte(',')
		}
		v.b.WriteByte('"')
		v.b.Write(Escape(val))
		v.b.WriteByte('"')
	}
	v.b.WriteByte(']')
}

func (v *detailVisitor) writeComma(pos int) {
	if pos > 0 {
		v.b.WriteByte(',')
	}
}

Changes to encoder/jsonenc/jsonenc.go.

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

26
27
28
29
30
31
32
11
12
13
14
15
16
17

18
19
20
21
22
23

24
25
26
27
28
29
30
31







-






-
+







// Package jsonenc encodes the abstract syntax tree into some JSON formats.
package jsonenc

import (
	"bytes"
	"io"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderJSON, encoder.Info{
	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

Changes to encoder/nativeenc/nativeenc.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

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

44
45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27

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

42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77

78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93








































































































94
95
96
97
98
99
100
101
102

103
104
105
106
107
108
109
110
111
112
113
114
115
116

117
118
119
120
121
122
123
124
125







-








-
+













-
+







-
+



















-
+







-
+















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









-
+












+
-
+
+








import (
	"fmt"
	"io"
	"sort"
	"strconv"

	"zettelstore.de/z/api"
	"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(api.EncoderNative, encoder.Info{
	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.walkInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle))
	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.walkBlockSlice(zn.Ast)
	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.walkBlockSlice(bs)
	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.walkInlineSlice(is)
	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}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.ParaNode:
		v.b.WriteString("[Para ")
		v.walkInlineSlice(n.Inlines)
		v.b.WriteByte(']')
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.b.WriteStrings("[Heading ", strconv.Itoa(n.Level), " \"", n.Slug, "\"")
		v.visitAttributes(n.Attrs)
		v.b.WriteByte(' ')
		v.walkInlineSlice(n.Inlines)
		v.b.WriteByte(']')
	case *ast.HRuleNode:
		v.b.WriteString("[Hrule")
		v.visitAttributes(n.Attrs)
		v.b.WriteByte(']')
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.BLOBNode:
		v.b.WriteString("[BLOB \"")
		v.writeEscaped(n.Title)
		v.b.WriteString("\" \"")
		v.writeEscaped(n.Syntax)
		v.b.WriteString("\" \"")
		v.b.WriteBase64(n.Blob)
		v.b.WriteString("\"]")
	case *ast.TextNode:
		v.b.WriteString("Text \"")
		v.writeEscaped(n.Text)
		v.b.WriteByte('"')
	case *ast.TagNode:
		v.b.WriteString("Tag \"")
		v.writeEscaped(n.Tag)
		v.b.WriteByte('"')
	case *ast.SpaceNode:
		v.b.WriteString("Space")
		if l := len(n.Lexeme); l > 1 {
			v.b.WriteByte(' ')
			v.b.WriteString(strconv.Itoa(l))
		}
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteString("Break")
		} else {
			v.b.WriteString("Space")
		}
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.ImageNode:
		v.visitImage(n)
	case *ast.CiteNode:
		v.b.WriteString("Cite")
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" \"")
		v.writeEscaped(n.Key)
		v.b.WriteByte('"')
		if len(n.Inlines) > 0 {
			v.b.WriteString(" [")
			v.walkInlineSlice(n.Inlines)
			v.b.WriteByte(']')
		}
	case *ast.FootnoteNode:
		v.b.WriteString("Footnote")
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" [")
		v.walkInlineSlice(n.Inlines)
		v.b.WriteByte(']')
	case *ast.MarkNode:
		v.b.WriteString("Mark")
		if len(n.Text) > 0 {
			v.b.WriteString(" \"")
			v.writeEscaped(n.Text)
			v.b.WriteByte('"')
		}
	case *ast.FormatNode:
		v.b.Write(mapFormatKind[n.Kind])
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" [")
		v.walkInlineSlice(n.Inlines)
		v.b.WriteByte(']')
	case *ast.LiteralNode:
		kind, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown literal kind %v", n.Kind))
		}
		v.b.Write(kind)
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" \"")
		v.writeEscaped(n.Text)
		v.b.WriteByte('"')
	default:
		return v
	}
	return nil
}

var (
	rawBackslash   = []byte{'\\', '\\'}
	rawDoubleQuote = []byte{'\\', '"'}
	rawNewline     = []byte{'\\', 'n'}
)

func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) {
	if withTitle {
		v.b.WriteString("[Title ")
		v.walkInlineSlice(parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")))
		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.writeComma(i)
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.b.WriteByte('[')
		v.b.WriteStrings(p.Key, " \"")
		v.writeEscaped(p.Value)
		v.b.WriteString("\"]")
	}
	v.level--
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
139
140
141
142
143
144
145
146
147
148
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







+
+
+
+
+
+
+
-
+





+
-
-
+
+

-
+

-
+











-
+





+
-
-
+
+

-
+

-
+





-
+






-
+






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





+
-
-
+
+


+
-
+
+



-
+
-
-
-
-
-
-







+
-
+



+
-
+
+


-
+










-
-
-
-
+
-
-
-











+
-
+






+
-
+
+





+
-
+
+



+
-
+
+



















-
+



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












+
-
+


-
+










-
+




+
-
+


-
+



















-
+




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

















+
+
+
+
+
+
+
+
+
-
+







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





-
+


-
+

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



















+
-
+



+
-
+
+







+







			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 mapVerbatimKind = map[ast.VerbatimKind][]byte{
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) {
	kind, ok := mapVerbatimKind[vn.Kind]
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	code, ok := verbatimCode[vn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind))
		panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
	}
	v.b.Write(kind)
	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 mapRegionKind = map[ast.RegionKind][]byte{
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) {
	kind, ok := mapRegionKind[rn.Kind]
func (v *visitor) VisitRegion(rn *ast.RegionNode) {
	code, ok := regionCode[rn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %v", rn.Kind))
		panic(fmt.Sprintf("Unknown region code %v", rn.Code))
	}
	v.b.Write(kind)
	v.b.Write(code)
	v.visitAttributes(rn.Attrs)
	v.level++
	v.writeNewLine()
	v.b.WriteByte('[')
	v.level++
	v.walkBlockSlice(rn.Blocks)
	v.acceptBlockSlice(rn.Blocks)
	v.level--
	v.b.WriteByte(']')
	if len(rn.Inlines) > 0 {
		v.b.WriteByte(',')
		v.writeNewLine()
		v.b.WriteString("[Cite ")
		v.walkInlineSlice(rn.Inlines)
		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 mapNestedListKind = map[ast.NestedListKind][]byte{
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(mapNestedListKind[ln.Kind])
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.writeComma(i)
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.level++
		v.b.WriteByte('[')
		for i, in := range item {
		v.acceptItemSlice(item)
			if i > 0 {
				v.b.WriteByte(',')
				v.writeNewLine()
			}
			ast.Walk(v, in)
		}
		v.b.WriteByte(']')
		v.level--
	}
	v.level--
	v.b.WriteByte(']')
}

// VisitDescriptionList emits a native description list.
func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	v.b.WriteString("[DescriptionList")
	v.level++
	for i, descr := range dn.Descriptions {
		if i > 0 {
		v.writeComma(i)
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.b.WriteString("[Term [")
		v.walkInlineSlice(descr.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()
				for i, dn := range b {
					if i > 0 {
						v.b.WriteByte(',')
						v.writeNewLine()
				v.acceptDescriptionSlice(b)
					}
					ast.Walk(v, dn)
				}
				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) {
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.writeComma(i)
				v.b.WriteByte(',')
			}
			v.writeCell(cell)
		}
		v.b.WriteString("],")
	}
	for i, row := range tn.Rows {
		if i > 0 {
		v.writeComma(i)
			v.b.WriteByte(',')
		}
		v.writeNewLine()
		v.b.WriteString("[Row ")
		for j, cell := range row {
			if j > 0 {
			v.writeComma(j)
				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.walkInlineSlice(cell.Inlines)
		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) {
func (v *visitor) VisitLink(ln *ast.LinkNode) {
	ln, n := v.env.AdaptLink(ln)
	if n != nil {
		ast.Walk(v, n)
		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.walkInlineSlice(ln.Inlines)
		v.acceptInlineSlice(ln.Inlines)
	}
	v.b.WriteByte(']')
}

// VisitImage writes native code for images.
func (v *visitor) visitImage(in *ast.ImageNode) {
func (v *visitor) VisitImage(in *ast.ImageNode) {
	in, n := v.env.AdaptImage(in)
	if n != nil {
		ast.Walk(v, n)
		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.walkInlineSlice(in.Inlines)
		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 mapFormatKind = map[ast.FormatKind][]byte{
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 mapLiteralKind = map[ast.LiteralKind][]byte{
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) walkBlockSlice(bns ast.BlockSlice) {
func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
	for i, bn := range bns {
		if i > 0 {
			v.b.WriteByte(',')
			v.writeNewLine()
		}
		ast.Walk(v, bn)
		bn.Accept(v)
	}
}
func (v *visitor) walkInlineSlice(ins ast.InlineSlice) {
func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
	for i, in := range ins {
		if i > 0 {
			v.b.WriteByte(',')
		v.writeComma(i)
		ast.Walk(v, in)
			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 i, k := range keys {
	for _, k := range keys {
		if k == "" {
			continue
		}
		if !first {
		v.writeComma(i)
			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++ {
561
562
563
564
565
566
567
568
569
570
571
572
573
608
609
610
611
612
613
614













-
-
-
-
-
-
		}
		v.b.WriteString(s[last:i])
		v.b.Write(b)
		last = i + 1
	}
	v.b.WriteString(s[last:])
}

func (v *visitor) writeComma(pos int) {
	if pos > 0 {
		v.b.WriteByte(',')
	}
}

Changes to encoder/rawenc/rawenc.go.

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

25
26
27
28
29
30
31
10
11
12
13
14
15
16

17
18
19
20
21
22

23
24
25
26
27
28
29
30







-






-
+








// Package rawenc encodes the abstract syntax tree as raw content.
package rawenc

import (
	"io"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderRaw, encoder.Info{
	encoder.Register("raw", encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &rawEncoder{} },
	})
}

type rawEncoder struct{}

// WriteZettel writes the encoded zettel to the writer.

Changes to encoder/textenc/textenc.go.

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

26
27
28
29
30
31
32
10
11
12
13
14
15
16

17
18
19
20
21
22
23

24
25
26
27
28
29
30
31







-







-
+








// Package textenc encodes the abstract syntax tree into its text.
package textenc

import (
	"io"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
)

func init() {
	encoder.Register(api.EncoderText, encoder.Info{
	encoder.Register("text", encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &textEncoder{} },
	})
}

type textEncoder struct{}

// WriteZettel writes metadata and content.
82
83
84
85
86
87
88
89

90
91
92
93
94
95
96
97
98
99
100
101
102

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122





































123
124
125
126
127
128
129
130
131
132
133
134
135

















136
137
138
139
140
141
142
143
144
145
146
147
148
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

















81
82
83
84
85
86
87

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102




















103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139













140
141
142
143
144
145
146
147
148
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







-
+













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


+
-
+
-
-
+
+
-


+
+
+



+
-
-
+
+
+


+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
	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) {
	v := newVisitor(w)
	ast.WalkInlineSlice(v, is)
	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
}

func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewBufWriter(w)}
}

// VisitPara emits text code for a paragraph
func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.VerbatimNode:
		if n.Kind == ast.VerbatimComment {
			return nil
		}
		for i, line := range n.Lines {
			v.writePosChar(i, '\n')
			v.b.WriteString(line)
		}
		return nil
	case *ast.RegionNode:
		v.acceptBlockSlice(n.Blocks)
		if len(n.Inlines) > 0 {
			v.b.WriteByte('\n')
			ast.WalkInlineSlice(v, n.Inlines)
		}
		return nil
	case *ast.NestedListNode:
		for i, item := range n.Items {
func (v *visitor) VisitPara(pn *ast.ParaNode) {
	v.acceptInlineSlice(pn.Inlines)
}

// VisitVerbatim emits text for verbatim lines.
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	if vn.Code == ast.VerbatimComment {
		return
	}
	for i, line := range vn.Lines {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.b.WriteString(line)
	}
}

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

// VisitHeading writes the text code for a heading.
func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
	v.acceptInlineSlice(hn.Inlines)
}

// VisitHRule writes nothing for a horizontal rule.
func (v *visitor) VisitHRule(hn *ast.HRuleNode) {}

// VisitNestedList writes text code for lists and blockquotes.
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)
			}
		}
		return nil
	case *ast.DescriptionListNode:
		for i, descr := range n.Descriptions {
			v.writePosChar(i, '\n')
			ast.WalkInlineSlice(v, descr.Term)
			for _, b := range descr.Descriptions {
				v.b.WriteByte('\n')
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.acceptItemSlice(item)
	}
}

// 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.acceptInlineSlice(descr.Term)

		for _, b := range descr.Descriptions {
			v.b.WriteByte('\n')
				for k, d := range b {
					v.writePosChar(k, '\n')
					ast.Walk(v, d)
				}
			}
		}
		return nil
	case *ast.TableNode:
		if len(n.Header) > 0 {
			v.writeRow(n.Header)
			v.b.WriteByte('\n')
		}
		for i, row := range n.Rows {
			v.writePosChar(i, '\n')
			v.writeRow(row)
		}
		return nil
	case *ast.TextNode:
		v.b.WriteString(n.Text)
		return nil
	case *ast.TagNode:
		v.b.WriteStrings("#", n.Tag)
		return nil
	case *ast.SpaceNode:
		v.b.WriteByte(' ')
		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteByte('\n')
		} else {
			v.b.WriteByte(' ')
		}
		return nil
	case *ast.LinkNode:
		if !n.OnlyRef {
			ast.WalkInlineSlice(v, n.Inlines)
		}
		return nil
	case *ast.FootnoteNode:
		v.b.WriteByte(' ')
		return v // No 'return nil' to write text
	case *ast.LiteralNode:
		if n.Kind != ast.LiteralComment {
			v.b.WriteString(n.Text)
		}
	}
	return v
			v.acceptDescriptionSlice(b)
		}
	}
}

// VisitTable emits a text table.
func (v *visitor) VisitTable(tn *ast.TableNode) {
	if len(tn.Header) > 0 {
		for i, cell := range tn.Header {
			if i > 0 {
				v.b.WriteByte(' ')
			}
			v.acceptInlineSlice(cell.Inlines)
		}
		v.b.WriteByte('\n')
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		for j, cell := range row {
			if j > 0 {
				v.b.WriteByte(' ')
			}
			v.acceptInlineSlice(cell.Inlines)
		}
	}
}

// VisitBLOB writes nothing, because it contains no text.
func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {}

// VisitText writes text content.
func (v *visitor) VisitText(tn *ast.TextNode) {
	v.b.WriteString(tn.Text)
}

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

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

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

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

// VisitFormat write text code for formatting text.
func (v *visitor) VisitFormat(fn *ast.FormatNode) {
	v.acceptInlineSlice(fn.Inlines)
}

// VisitLiteral write text code for literal inline text.
func (v *visitor) writeRow(row ast.TableRow) {
func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
	for i, cell := range row {
		v.writePosChar(i, ' ')
	if ln.Code != ast.LiteralComment {
		v.b.WriteString(ln.Text)
		ast.WalkInlineSlice(v, cell.Inlines)
	}
}

// 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.writePosChar(i, '\n')
		ast.Walk(v, bn)
			v.b.WriteByte('\n')
		}
		bn.Accept(v)
	}
}
func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
	for i, in := range ins {
		if i > 0 {
			v.b.WriteByte('\n')

func (v *visitor) writePosChar(pos int, ch byte) {
	if pos > 0 {
		v.b.WriteByte(ch)
	}
}
		}
		in.Accept(v)
	}
}
func (v *visitor) acceptDescriptionSlice(dns ast.DescriptionSlice) {
	for i, dn := range dns {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		dn.Accept(v)
	}
}
func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
	for _, in := range ins {
		in.Accept(v)
	}
}

Changes to encoder/zmkenc/zmkenc.go.

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

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

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

60
61
62
63
64
65
66
67

68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

86

87
88
89
90
91
92
93





94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141

142
143
144
145



146
147
148
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
12
13
14
15
16
17
18

19
20
21
22
23
24

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

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

58
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

86







87
88
89
90
91
















































92




93
94
95
96
97
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112

113
114

115
116

117
118

119
120
121


122
123
124
125

126
127
128
129
130
131

132
133
134
135
136

137
138
139
140
141
142
143
144
145
146
147
148

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







-






-
+















-
+
















-
+







-
+


















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










-
+





+
-
+

-
+

-
+

-
+


-
-
+
+


-
+




+
-
+




-
+
+
+
+
+
+
+
+




-
+





+
-
-
+
+












-
+






+
-
+


-
+




+
-
+
+












+
-
+







-
+












-
+





+
+
+
+
+
+
+
+
+
+



















+
-
+







package zmkenc

import (
	"fmt"
	"io"
	"sort"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderZmk, encoder.Info{
	encoder.Register("zmk", encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &zmkEncoder{} },
	})
}

type zmkEncoder struct{}

// 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)
	} else {
		zn.Meta.WriteAsHeader(&v.b, true)
	}
	ast.WalkBlockSlice(v, zn.Ast)
	v.acceptBlockSlice(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 (ze *zmkEncoder) 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)
	ast.WalkBlockSlice(v, bs)
	v.acceptBlockSlice(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)
	ast.WalkInlineSlice(v, is)
	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
	prefix []byte
	enc    *zmkEncoder
}

func newVisitor(w io.Writer, enc *zmkEncoder) *visitor {
	return &visitor{
		b:   encoder.NewBufWriter(w),
		enc: enc,
	}
}

// VisitPara emits HTML code for a paragraph: <p>...</p>
func (v *visitor) Visit(node ast.Node) ast.Visitor {
func (v *visitor) VisitPara(pn *ast.ParaNode) {
	switch n := node.(type) {
	case *ast.ParaNode:
		ast.WalkInlineSlice(v, n.Inlines)
		v.b.WriteByte('\n')
		if len(v.prefix) == 0 {
			v.b.WriteByte('\n')
		}
	v.acceptInlineSlice(pn.Inlines)
	v.b.WriteByte('\n')
	if len(v.prefix) == 0 {
		v.b.WriteByte('\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("---")
		v.visitAttributes(n.Attrs)
		v.b.WriteByte('\n')
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.BLOBNode:
		v.b.WriteStrings(
			"%% Unable to display BLOB with title '", n.Title,
			"' and syntax '", n.Syntax, "'\n")
	case *ast.TextNode:
		v.visitText(n)
	case *ast.TagNode:
		v.b.WriteStrings("#", n.Tag)
	case *ast.SpaceNode:
		v.b.WriteString(n.Lexeme)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.ImageNode:
		v.visitImage(n)
	case *ast.CiteNode:
		v.visitCite(n)
	case *ast.FootnoteNode:
		v.b.WriteString("[^")
		ast.WalkInlineSlice(v, n.Inlines)
		v.b.WriteByte(']')
		v.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		v.b.WriteStrings("[!", n.Text, "]")
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
}
	return nil
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {

// 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.WriteByte('\n')
	for _, line := range vn.Lines {
		v.b.WriteStrings(line, "\n")
	}
	v.b.WriteString("```\n")
}

var mapRegionKind = map[ast.RegionKind]string{
var regionCode = map[ast.RegionCode]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
	kind, ok := mapRegionKind[rn.Kind]
	code, ok := regionCode[rn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %d", rn.Kind))
		panic(fmt.Sprintf("Unknown region code %d", rn.Code))
	}
	v.b.WriteString(kind)
	v.b.WriteString(code)
	v.visitAttributes(rn.Attrs)
	v.b.WriteByte('\n')
	ast.WalkBlockSlice(v, rn.Blocks)
	v.b.WriteString(kind)
	v.acceptBlockSlice(rn.Blocks)
	v.b.WriteString(code)
	if len(rn.Inlines) > 0 {
		v.b.WriteByte(' ')
		ast.WalkInlineSlice(v, rn.Inlines)
		v.acceptInlineSlice(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(' ')
	ast.WalkInlineSlice(v, hn.Inlines)
	v.acceptInlineSlice(hn.Inlines)
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte('\n')
}

// VisitHRule writes HTML code for a horizontal rule: <hr>.
func (v *visitor) VisitHRule(hn *ast.HRuleNode) {
	v.b.WriteString("---")
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte('\n')
}

var mapNestedListKind = map[ast.NestedListKind]byte{
var listCode = map[ast.NestedListCode]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, mapNestedListKind[ln.Kind])
func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
	v.prefix = append(v.prefix, listCode[ln.Code])
	for _, item := range ln.Items {
		v.b.Write(v.prefix)
		v.b.WriteByte(' ')
		for i, in := range item {
			if i > 0 {
				if _, ok := in.(*ast.ParaNode); ok {
					v.b.WriteByte('\n')
					for j := 0; j <= len(v.prefix); j++ {
						v.b.WriteByte(' ')
					}
				}
			}
			ast.Walk(v, in)
			in.Accept(v)
		}
	}
	v.prefix = v.prefix[:len(v.prefix)-1]
	v.b.WriteByte('\n')
}

// VisitDescriptionList emits a HTML description list.
func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
	for _, descr := range dn.Descriptions {
		v.b.WriteString("; ")
		ast.WalkInlineSlice(v, descr.Term)
		v.acceptInlineSlice(descr.Term)
		v.b.WriteByte('\n')

		for _, b := range descr.Descriptions {
			v.b.WriteString(": ")
			for _, dn := range b {
			ast.WalkDescriptionSlice(v, b)
				dn.Accept(v)
			}
			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) {
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])
			}
			ast.WalkInlineSlice(v, cell.Inlines)
			v.acceptInlineSlice(cell.Inlines)
			if colAlign != ast.AlignDefault {
				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])
			}
			ast.WalkInlineSlice(v, cell.Inlines)
			v.acceptInlineSlice(cell.Inlines)
		}
		v.b.WriteByte('\n')
	}
	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 '",
		bn.Title,
		"' and syntax '",
		bn.Syntax,
		"'\n")
}

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







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












+
-
+


-
+





+
-
+



-
+






+
-
+



-
+





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

















+
-
-
+
+

-
+


-
+





-
-
-
+
+
+



+
-
-
+
+













-
+









+
+
+
+
+
+
+
+
+
+
+







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

// 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 {
		ast.WalkInlineSlice(v, ln.Inlines)
		v.acceptInlineSlice(ln.Inlines)
		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) VisitImage(in *ast.ImageNode) {
	if in.Ref != nil {
		v.b.WriteString("{{")
		if len(in.Inlines) > 0 {
			ast.WalkInlineSlice(v, in.Inlines)
			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) {
func (v *visitor) VisitCite(cn *ast.CiteNode) {
	v.b.WriteStrings("[@", cn.Key)
	if len(cn.Inlines) > 0 {
		v.b.WriteString(", ")
		ast.WalkInlineSlice(v, cn.Inlines)
		v.acceptInlineSlice(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(']')
	v.visitAttributes(fn.Attrs)
}

// VisitMark writes HTML code to mark a position.
func (v *visitor) VisitMark(mn *ast.MarkNode) {
	v.b.WriteStrings("[!", mn.Text, "]")
}

var mapFormatKind = map[ast.FormatKind][]byte{
var formatCode = map[ast.FormatCode][]byte{
	ast.FormatItalic:    []byte("//"),
	ast.FormatEmph:      []byte("//"),
	ast.FormatBold:      []byte("**"),
	ast.FormatStrong:    []byte("**"),
	ast.FormatUnder:     []byte("__"),
	ast.FormatInsert:    []byte("__"),
	ast.FormatStrike:    []byte("~~"),
	ast.FormatDelete:    []byte("~~"),
	ast.FormatSuper:     []byte("^^"),
	ast.FormatSub:       []byte(",,"),
	ast.FormatQuotation: []byte("<<"),
	ast.FormatQuote:     []byte("\"\""),
	ast.FormatSmall:     []byte(";;"),
	ast.FormatSpan:      []byte("::"),
	ast.FormatMonospace: []byte("''"),
}

// VisitFormat write HTML code for formatting text.
func (v *visitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
func (v *visitor) VisitFormat(fn *ast.FormatNode) {
	code, ok := formatCode[fn.Code]
	if !ok {
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))
		panic(fmt.Sprintf("Unknown format code %d", fn.Code))
	}
	attrs := fn.Attrs
	switch fn.Kind {
	switch fn.Code {
	case ast.FormatEmph, ast.FormatStrong, ast.FormatInsert, ast.FormatDelete:
		attrs = attrs.Clone()
		attrs.Set("-", "")
	}

	v.b.Write(kind)
	ast.WalkInlineSlice(v, fn.Inlines)
	v.b.Write(kind)
	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.Kind {
func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
	switch ln.Code {
	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.writeEscaped(ln.Text, '`')
		v.b.WriteString("``{=html,.warning}")
	default:
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
		panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
	}
}

func (v *visitor) writeLiteral(code byte, attrs *ast.Attributes, text string) {
	v.b.WriteBytes(code, code)
	v.writeEscaped(text, code)
	v.b.WriteBytes(code, code)
	v.visitAttributes(attrs)
}

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 {
		return
	}
	keys := make([]string, 0, len(a.Attrs))

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







-
-
-
+
+
+


module zettelstore.de/z

go 1.16

require (
	github.com/fsnotify/fsnotify v1.4.9
	github.com/pascaldekloe/jwt v1.10.0
	github.com/yuin/goldmark v1.4.0
	golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
	golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
	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
)

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


17
18
19
20




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

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



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/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.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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=
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.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
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/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

Changes to input/input_test.go.

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

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

41
42
43
44
45
46
47







-




















-







import (
	"testing"

	"zettelstore.de/z/input"
)

func TestEatEOL(t *testing.T) {
	t.Parallel()
	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) {
	t.Parallel()
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"a", ""},
		{"&amp;", "&"},

Deleted input/runes.go.

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





















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

// IsSpace returns true if rune is a whitespace.
func IsSpace(ch rune) bool {
	switch ch {
	case ' ', '\t':
		return true
	}
	return false
}

Deleted 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


































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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/box"
	"zettelstore.de/z/kernel"
)

type boxService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       box.Manager
	createManager kernel.CreateBoxManagerFunc
}

func (ps *boxService) Initialize() {
	ps.descr = descriptionMap{
		kernel.BoxDefaultDirType: {
			"Default directory box type",
			ps.noFrozen(func(val string) interface{} {
				switch val {
				case kernel.BoxDirTypeNotify, kernel.BoxDirTypeSimple:
					return val
				}
				return nil
			}),
			true,
		},
		kernel.BoxURIs: {
			"Box 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.BoxDefaultDirType: kernel.BoxDirTypeNotify,
	}
}

func (ps *boxService) Start(kern *myKernel) error {
	boxURIs := make([]*url.URL, 0, 4)
	format := kernel.BoxURIs + "%d"
	for i := 1; ; i++ {
		u := ps.GetNextConfig(fmt.Sprintf(format, 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.rtConfig)
	if err != nil {
		kern.doLog("Unable to create box manager:", err)
		return err
	}
	kern.doLog("Start Box Manager:", mgr.Location())
	if err := mgr.Start(context.Background()); err != nil {
		kern.doLog("Unable to start box manager:", 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(kern *myKernel) error {
	kern.doLog("Stop Box 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 *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: fmt.Sprintf("%v", st.ReadOnly)},
		{Key: "Managed boxes", Value: fmt.Sprintf("%v", st.NumManagedBoxes)},
		{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: "Duration last re-index", Value: fmt.Sprintf("%vms", st.DurLastReload.Milliseconds())},
		{Key: "Indexes since last re-index", Value: fmt.Sprintf("%v", st.IndexesSinceReload)},
		{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 *boxService) DumpIndex(w io.Writer) {
	ps.manager.Dump(w)
}

Changes to kernel/impl/cfg.go.

10
11
12
13
14
15
16

17
18
19
20
21
22
23

24
25
26
27
28
29
30
10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31







+



-



+








// Package impl provides the kernel implementation.
package impl

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/place"
)

type configService struct {
	srvConfig
	mxService sync.RWMutex
	rtConfig  *myConfig
}
43
44
45
46
47
48
49
50
51
52














53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
44
45
46
47
48
49
50



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91







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



















+







				if vis == meta.VisibilityUnknown {
					return nil
				}
				return vis
			},
			true,
		},
		meta.KeyExpertMode:     {"Expert mode", parseBool, true},
		meta.KeyFooterHTML:     {"Footer HTML", parseString, true},
		meta.KeyHomeZettel:     {"Home zettel", parseZid, 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
				}
				return iVal
			},
			true,
		},
		meta.KeyMarkerExternal: {"Marker external URL", parseString, true},
		meta.KeySiteName:       {"Site name", parseString, true},
		meta.KeyYAMLHeader:     {"YAML header", parseBool, true},
		meta.KeyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) interface{} { return strings.Fields(val) },
			true,
		},
	}
	cs.next = interfaceMap{
		meta.KeyDefaultCopyright:  "",
		meta.KeyDefaultLang:       meta.ValueLangEN,
		meta.KeyDefaultRole:       meta.ValueRoleZettel,
		meta.KeyDefaultSyntax:     meta.ValueSyntaxZmk,
		meta.KeyDefaultTitle:      "Untitled",
		meta.KeyDefaultVisibility: meta.VisibilityLogin,
		meta.KeyExpertMode:        false,
		meta.KeyFooterHTML:        "",
		meta.KeyHomeZettel:        id.DefaultHomeZid,
		meta.KeyListPageSize:      0,
		meta.KeyMarkerExternal:    "&#10138;",
		meta.KeySiteName:          "Zettelstore",
		meta.KeyYAMLHeader:        false,
		meta.KeyZettelFileSyntax:  nil,
	}
}

102
103
104
105
106
107
108
109
110


111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

129
130
131
132
133

134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150



151
152
153
154
155
156
157
115
116
117
118
119
120
121


122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

141
142
143
144
145

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160



161
162
163
164
165
166
167
168
169
170







-
-
+
+

















-
+




-
+














-
-
-
+
+
+







	return nil
}

func (cs *configService) GetStatistics() []kernel.KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {
	cs.rtConfig.setBox(mgr)
func (cs *configService) setPlace(mgr place.Manager) {
	cs.rtConfig.setPlace(mgr)
}

// 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{
		orig: orig,
		data: orig.Clone(),
	}
	return &cfg
}
func (cfg *myConfig) setBox(mgr box.Manager) {
func (cfg *myConfig) setPlace(mgr place.Manager) {
	mgr.RegisterObserver(cfg.observe)
	cfg.doUpdate(mgr)
}

func (cfg *myConfig) doUpdate(p box.Box) error {
func (cfg *myConfig) doUpdate(p place.Place) error {
	m, err := p.GetMeta(context.Background(), cfg.data.Zid)
	if err != nil {
		return err
	}
	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 (cfg *myConfig) observe(ci box.UpdateInfo) {
	if ci.Reason == box.OnReload || ci.Zid == id.ConfigurationZid {
		go func() { cfg.doUpdate(ci.Box) }()
func (cfg *myConfig) observe(ci place.UpdateInfo) {
	if ci.Reason == place.OnReload || ci.Zid == id.ConfigurationZid {
		go func() { cfg.doUpdate(ci.Place) }()
	}
}

var defaultKeys = map[string]string{
	meta.KeyCopyright: meta.KeyDefaultCopyright,
	meta.KeyLang:      meta.KeyDefaultLang,
	meta.KeyLicense:   meta.KeyDefaultLicense,
165
166
167
168
169
170
171
172
173
174
175




176
177
178
179
180
181
182
178
179
180
181
182
183
184




185
186
187
188
189
190
191
192
193
194
195







-
-
-
-
+
+
+
+







	if cfg == nil {
		return m
	}
	result := m
	cfg.mx.RLock()
	for k, d := range defaultKeys {
		if _, ok := result.Get(k); !ok {
			if val, ok := cfg.data.Get(d); ok && val != "" {
				if result == m {
					result = m.Clone()
				}
			if result == m {
				result = m.Clone()
			}
			if val, ok := cfg.data.Get(d); ok {
				result.Set(k, val)
			}
		}
	}
	cfg.mx.RUnlock()
	return result
}
242
243
244
245
246
247
248













249
250
251
252
253
254
255
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







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







func (cfg *myConfig) GetMarkerExternal() string {
	return cfg.getString(meta.KeyMarkerExternal)
}

// 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.
// A value less or equal to zero signals no limit.
func (cfg *myConfig) GetListPageSize() int {
	cfg.mx.RLock()
	defer cfg.mx.RUnlock()

	if value, ok := cfg.data.GetNumber(meta.KeyListPageSize); ok {
		return value
	}
	value, _ := cfg.orig.GetNumber(meta.KeyListPageSize)
	return value
}

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

Changes to kernel/impl/cmd.go.

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
63
64
65
66
67
68
69




70
71
72
73
74
75
76







-
-
-
-







			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 {
		sess.printRow(table[0], maxLen, "|=", " | ", ' ')
281
282
283
284
285
286
287
288

289
290
291
292
293
294
295
277
278
279
280
281
282
283

284
285
286
287
288
289
290
291







-
+







		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.usage(cmd, "SERVICE KEY VALUE")
		sess.println("Usage:", cmd, "SERIVCE KEY VALUE")
		return true
	}
	srvD, ok := sess.kern.srvNames[args[0]]
	if !ok {
		sess.println("Unknown service:", args[0])
		return true
	}
353
354
355
356
357
358
359
360

361
362
363
364
365
366
367
349
350
351
352
353
354
355

356
357
358
359
360
361
362
363







-
+







		sess.println(err.Error())
	}
	return true
}

func cmdStat(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.usage(cmd, "SERVICE")
		sess.println("Usage:", cmd, "SERVICE")
		return true
	}
	srvD, ok := sess.kern.srvNames[args[0]]
	if !ok {
		sess.println("Unknown service", args[0])
		return true
	}
375
376
377
378
379
380
381
382

383
384
385
386
387
388
389
371
372
373
374
375
376
377

378
379
380
381
382
383
384
385







-
+







	}
	sess.printTable(table)
	return true
}

func lookupService(sess *cmdSession, cmd string, args []string) (kernel.Service, bool) {
	if len(args) == 0 {
		sess.usage(cmd, "SERVICE")
		sess.println("Usage:", cmd, "SERVICE")
		return 0, false
	}
	srvD, ok := sess.kern.srvNames[args[0]]
	if !ok {
		sess.println("Unknown service", args[0])
		return 0, false
	}
434
435
436
437
438
439
440
441

442
443
444
445
446
447
448
430
431
432
433
434
435
436

437
438
439
440
441
442
443
444







-
+








func cmdDumpIndex(sess *cmdSession, cmd string, args []string) bool {
	sess.kern.DumpIndex(sess.w)
	return true
}
func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.usage(cmd, "RECOVER")
		sess.println("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
	}

Changes to kernel/impl/config.go.

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







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



















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







func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue {
	return cfg.getConfigList(true, cfg.GetNextConfig)
}
func (cfg *srvConfig) getConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue {
	if len(cfg.descr) == 0 {
		return nil
	}
	keys := cfg.getSortedConfigKeys(all, getConfig)
	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
			}
			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 keys
	return result
}

func (cfg *srvConfig) Freeze() {
	cfg.mxConfig.Lock()
	cfg.frozen = true
	cfg.mxConfig.Unlock()
}

Changes to kernel/impl/impl.go.

30
31
32
33
34
35
36
37
38
39
40
41





42
43
44
45
46
47
48
30
31
32
33
34
35
36





37
38
39
40
41
42
43
44
45
46
47
48







-
-
-
-
-
+
+
+
+
+







type myKernel struct {
	// started   bool
	wg        sync.WaitGroup
	mx        sync.RWMutex
	interrupt chan os.Signal
	debug     bool

	core coreService
	cfg  configService
	auth authService
	box  boxService
	web  webService
	core  coreService
	cfg   configService
	auth  authService
	place placeService
	web   webService

	srvs     map[kernel.Service]serviceDescr
	srvNames map[string]serviceData
	depStart serviceDependency
	depStop  serviceDependency // reverse of depStart
}

66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89


90
91
92
93
94
95
96
97
98
99
100

101
102
103
104
105
106
107
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87


88
89
90
91
92
93
94
95
96
97
98
99

100
101
102
103
104
105
106
107







-
+














-
-
+
+










-
+







	kern := &myKernel{
		interrupt: make(chan os.Signal, 5),
	}
	kern.srvs = map[kernel.Service]serviceDescr{
		kernel.CoreService:   {&kern.core, "core"},
		kernel.ConfigService: {&kern.cfg, "config"},
		kernel.AuthService:   {&kern.auth, "auth"},
		kernel.BoxService:    {&kern.box, "box"},
		kernel.PlaceService:  {&kern.place, "place"},
		kernel.WebService:    {&kern.web, "web"},
	}
	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.srvNames[srvD.name] = serviceData{srvD.srv, key}
		srvD.srv.Initialize()
	}
	kern.depStart = serviceDependency{
		kernel.CoreService:   nil,
		kernel.ConfigService: {kernel.CoreService},
		kernel.AuthService:   {kernel.CoreService},
		kernel.BoxService:    {kernel.CoreService, kernel.ConfigService, kernel.AuthService},
		kernel.WebService:    {kernel.ConfigService, kernel.AuthService, kernel.BoxService},
		kernel.PlaceService:  {kernel.CoreService, kernel.ConfigService, kernel.AuthService},
		kernel.WebService:    {kernel.ConfigService, kernel.AuthService, kernel.PlaceService},
	}
	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) Start(headline bool, lineServer bool) {
func (kern *myKernel) Start(headline bool) {
	for _, srvD := range kern.srvs {
		srvD.srv.Freeze()
	}
	kern.wg.Add(1)
	signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM)
	go func() {
		// Wait for interrupt.
124
125
126
127
128
129
130
131
132
133
134
135




136
137
138
139
140
141
142
143
124
125
126
127
128
129
130





131
132
133
134

135
136
137
138
139
140
141







-
-
-
-
-
+
+
+
+
-







			kern.core.GetConfig(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")
		}
	}
	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)
	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.
}

305
306
307
308
309
310
311
312

313
314
315
316
317
318
319
303
304
305
306
307
308
309

310
311
312
313
314
315
316
317







-
+







				found[depSrv] = true
			}
		}
	}
	return append(result, srvD.srv)
}
func (kern *myKernel) DumpIndex(w io.Writer) {
	kern.box.DumpIndex(w)
	kern.place.DumpIndex(w)
}

type service interface {
	// Initialize the data for the service.
	Initialize()

	// ConfigDescriptions returns a sorted list of configuration descriptions.
353
354
355
356
357
358
359
360

361
362
363
364

365
366
351
352
353
354
355
356
357

358
359
360
361

362
363
364







-
+



-
+


	Stop(*myKernel) error
}

type serviceConfigDescription struct{ Key, Descr string }

func (kern *myKernel) SetCreators(
	createAuthManager kernel.CreateAuthManagerFunc,
	createBoxManager kernel.CreateBoxManagerFunc,
	createPlaceManager kernel.CreatePlaceManagerFunc,
	setupWebServer kernel.SetupWebServerFunc,
) {
	kern.auth.createManager = createAuthManager
	kern.box.createManager = createBoxManager
	kern.place.createManager = createPlaceManager
	kern.web.setupServer = setupWebServer
}

Added 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/web.go.

106
107
108
109
110
111
112
113

114
115
116
117
118
119
120
106
107
108
109
110
111
112

113
114
115
116
117
118
119
120







-
+







func (ws *webService) Start(kern *myKernel) error {
	listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
	urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string)
	persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool)
	secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool)

	srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, kern.cfg.rtConfig)
	err := kern.web.setupServer(srvw, kern.place.manager, kern.auth.manager, kern.cfg.rtConfig)
	if err != nil {
		kern.doLog("Unable to create Web Server:", err)
		return err
	}
	if kern.debug {
		srvw.SetDebug()
	}

Changes to kernel/kernel.go.

12
13
14
15
16
17
18
19
20
21



22
23
24
25
26
27
28

29
30
31
32
33
34
35
12
13
14
15
16
17
18



19
20
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35







-
-
-
+
+
+






-
+







package kernel

import (
	"io"
	"net/url"

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

// Kernel is the main internal service.
type Kernel interface {
	// Start the service.
	Start(headline bool, lineServer bool)
	Start(headline bool)

	// WaitForShutdown blocks the call until Shutdown is called.
	WaitForShutdown()

	// SetDebug to enable/disable debug mode
	SetDebug(enable bool) bool

63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

92
93
94
95
96
97
98
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

91
92
93
94
95
96
97
98







-
+




















-
+







	// 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, CreateBoxManagerFunc, SetupWebServerFunc)
	SetCreators(CreateAuthManagerFunc, CreatePlaceManagerFunc, 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

// Service specifies a service, e.g. web, ...
type Service uint8

// Constants for type Service.
const (
	_ Service = iota
	CoreService
	ConfigService
	AuthService
	BoxService
	PlaceService
	WebService
)

// Constants for core service system keys.
const (
	CoreGoArch    = "go-arch"
	CoreGoOS      = "go-os"
106
107
108
109
110
111
112
113

114
115
116


117
118
119

120
121
122


123
124
125
126
127
128
129
106
107
108
109
110
111
112

113
114


115
116
117
118

119
120


121
122
123
124
125
126
127
128
129







-
+

-
-
+
+


-
+

-
-
+
+








// Constants for authentication service keys.
const (
	AuthOwner    = "owner"
	AuthReadonly = "readonly"
)

// Constants for box service keys.
// Constants for place service keys.
const (
	BoxDefaultDirType = "defdirtype"
	BoxURIs           = "box-uri-"
	PlaceDefaultDirType = "defdirtype"
	PlaceURIs           = "place-uri-"
)

// Allowed values for BoxDefaultDirType
// Allowed values for PlaceDefaultDirType
const (
	BoxDirTypeNotify = "notify"
	BoxDirTypeSimple = "simple"
	PlaceDirTypeNotify = "notify"
	PlaceDirTypeSimple = "simple"
)

// Constants for web service keys.
const (
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"
	WebSecureCookie      = "secure"
137
138
139
140
141
142
143
144
145
146



147
148
149

150
151
152
153
154

155
156
157
137
138
139
140
141
142
143



144
145
146
147
148

149
150
151
152
153

154
155
156
157







-
-
-
+
+
+


-
+




-
+




// KeyValue is a pair of key and value.
type KeyValue struct{ Key, Value string }

// CreateAuthManagerFunc is called to create a new auth manager.
type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error)

// CreateBoxManagerFunc is called to create a new box manager.
type CreateBoxManagerFunc func(
	boxURIs []*url.URL,
// CreatePlaceManagerFunc is called to create a new place manager.
type CreatePlaceManagerFunc func(
	placeURIs []*url.URL,
	authManager auth.Manager,
	rtConfig config.Config,
) (box.Manager, error)
) (place.Manager, error)

// SetupWebServerFunc is called to create a new web service handler.
type SetupWebServerFunc func(
	webServer server.Server,
	boxManager box.Manager,
	placeManager place.Manager,
	authManager auth.Manager,
	rtConfig config.Config,
) error

Changes to parser/cleaner/cleaner.go.

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


28
29
30

31

32
33
34

35
36
37
38
39
40
41
42
43
44

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



































































61
62
63
64
65
66
67
68
69
70










71
72
73
74







75
76
77
78
79
80
81
11
12
13
14
15
16
17

18
19
20
21
22
23
24


25
26

27
28
29

30
31
32

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
















45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111










112
113
114
115
116
117
118
119
120
121




122
123
124
125
126
127
128
129
130
131
132
133
134
135







-







-
-
+
+
-


+
-
+


-
+










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







// Package cleaner provides funxtions to clean up the parsed AST.
package cleaner

import (
	"strconv"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/strfun"
)

// CleanupBlockSlice cleans the given block slice.
func CleanupBlockSlice(bs ast.BlockSlice) {
	cv := cleanupVisitor{
		textEnc: encoder.Create(api.EncoderText, nil),
	cv := &cleanupVisitor{
		textEnc: encoder.Create("text", nil),
		hasMark: false,
		doMark:  false,
	}
	t := ast.NewTopDownTraverser(cv)
	ast.WalkBlockSlice(&cv, bs)
	t.VisitBlockSlice(bs)
	if cv.hasMark {
		cv.doMark = true
		ast.WalkBlockSlice(&cv, bs)
		t.VisitBlockSlice(bs)
	}
}

type cleanupVisitor struct {
	textEnc encoder.Encoder
	ids     map[string]ast.Node
	hasMark bool
	doMark  bool
}

// VisitVerbatim does nothing.
func (cv *cleanupVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.HeadingNode:
		if cv.doMark || n == nil || n.Inlines == nil {
			return nil
		}
		var sb strings.Builder
		_, err := cv.textEnc.WriteInlines(&sb, n.Inlines)
		if err != nil {
			return nil
		}
		s := strfun.Slugify(sb.String())
		if len(s) > 0 {
			n.Slug = cv.addIdentifier(s, n)
		}
		return nil
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 {
		return
	}
	var sb strings.Builder
	_, err := cv.textEnc.WriteInlines(&sb, hn.Inlines)
	if err != nil {
		return
	}
	s := strfun.Slugify(sb.String())
	if len(s) > 0 {
		hn.Slug = cv.addIdentifier(s, hn)
	}
}

// VisitHRule does nothing.
func (cv *cleanupVisitor) VisitHRule(hn *ast.HRuleNode) {}

// VisitList does nothing.
func (cv *cleanupVisitor) VisitNestedList(ln *ast.NestedListNode) {}

// VisitDescriptionList does nothing.
func (cv *cleanupVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {}

// VisitPara does nothing.
func (cv *cleanupVisitor) VisitPara(pn *ast.ParaNode) {}

// 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.
func (cv *cleanupVisitor) VisitTag(tn *ast.TagNode) {}

// VisitSpace does nothing.
func (cv *cleanupVisitor) VisitSpace(sn *ast.SpaceNode) {}

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

// VisitImage collects the image links as a reference.
func (cv *cleanupVisitor) VisitImage(in *ast.ImageNode) {}

// 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
	case *ast.MarkNode:
		if !cv.doMark {
			cv.hasMark = true
			return nil
		}
		if n.Text == "" {
			n.Text = cv.addIdentifier("*", n)
			return nil
		}
		n.Text = cv.addIdentifier(n.Text, n)
	}
	if !cv.doMark {
		cv.hasMark = true
		return
	}
	if mn.Text == "" {
		mn.Text = cv.addIdentifier("*", mn)
		return
	}
	mn.Text = cv.addIdentifier(mn.Text, mn)
		return nil
	}
	return cv
}
}

// 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 {
	if cv.ids == nil {
		cv.ids = map[string]ast.Node{id: node}
		return id
	}
	if n, ok := cv.ids[id]; ok && n != node {

Changes to parser/markdown/markdown.go.

16
17
18
19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
34
35







-





+







	"fmt"
	"strings"

	gm "github.com/yuin/goldmark"
	gmAst "github.com/yuin/goldmark/ast"
	gmText "github.com/yuin/goldmark/text"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/runes"
)

func init() {
	parser.Register(&parser.Info{
		Name:         "markdown",
		AltNames:     []string{"md"},
		ParseBlocks:  parseBlocks,
46
47
48
49
50
51
52
53

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

53
54
55
56
57
58
59
60







-
+







	panic("markdown.parseInline not yet implemented")
}

func parseMarkdown(inp *input.Input) *mdP {
	source := []byte(inp.Src[inp.Pos:])
	parser := gm.DefaultParser()
	node := parser.Parse(gmText.NewReader(source))
	textEnc := encoder.Create(api.EncoderText, nil)
	textEnc := encoder.Create("text", nil)
	return &mdP{source: source, docNode: node, textEnc: textEnc}
}

type mdP struct {
	source  []byte
	docNode gmAst.Node
	textEnc encoder.Encoder
121
122
123
124
125
126
127
128

129
130
131
132
133
134
135
136
137
138
139
140

141
142
143
144
145
146
147
121
122
123
124
125
126
127

128
129
130
131
132
133
134
135
136
137
138
139

140
141
142
143
144
145
146
147







-
+











-
+







	return &ast.HRuleNode{
		Attrs: nil, //TODO
	}
}

func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode {
	return &ast.VerbatimNode{
		Kind:  ast.VerbatimProg,
		Code:  ast.VerbatimProg,
		Attrs: nil, //TODO
		Lines: p.acceptRawText(node),
	}
}

func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode {
	var attrs *ast.Attributes
	if language := node.Language(p.source); len(language) > 0 {
		attrs = attrs.Set("class", "language-"+cleanText(string(language), true))
	}
	return &ast.VerbatimNode{
		Kind:  ast.VerbatimProg,
		Code:  ast.VerbatimProg,
		Attrs: attrs,
		Lines: p.acceptRawText(node),
	}
}

func (p *mdP) acceptRawText(node gmAst.Node) []string {
	lines := node.Lines()
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
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







-
+







-
+


-
+













-
+







		result = append(result, string(line))
	}
	return result
}

func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *ast.NestedListNode {
	return &ast.NestedListNode{
		Kind: ast.NestedListQuote,
		Code: ast.NestedListQuote,
		Items: []ast.ItemSlice{
			p.acceptItemSlice(node),
		},
	}
}

func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode {
	kind := ast.NestedListUnordered
	code := ast.NestedListUnordered
	var attrs *ast.Attributes
	if node.IsOrdered() {
		kind = ast.NestedListOrdered
		code = ast.NestedListOrdered
		if node.Start != 1 {
			attrs = attrs.Set("start", fmt.Sprintf("%d", 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{
		Kind:  kind,
		Code:  code,
		Items: items,
		Attrs: attrs,
	}
}

func (p *mdP) acceptItemSlice(node gmAst.Node) ast.ItemSlice {
	result := make(ast.ItemSlice, 0, node.ChildCount())
219
220
221
222
223
224
225
226

227
228
229
230
231
232
233
219
220
221
222
223
224
225

226
227
228
229
230
231
232
233







-
+







		closure := string(node.ClosureLine.Value(p.source))
		if l := len(closure); l > 1 && closure[l-1] == '\n' {
			closure = closure[:l-1]
		}
		lines = append(lines, closure)
	}
	return &ast.VerbatimNode{
		Kind:  ast.VerbatimHTML,
		Code:  ast.VerbatimHTML,
		Lines: lines,
	}
}

func (p *mdP) acceptInlineSlice(node gmAst.Node) ast.InlineSlice {
	result := make(ast.InlineSlice, 0, node.ChildCount())
	for child := node.FirstChild(); child != nil; child = child.NextSibling() {
288
289
290
291
292
293
294
295

296
297
298
299
300
301
302
288
289
290
291
292
293
294

295
296
297
298
299
300
301
302







-
+







		return ast.InlineSlice{}
	}
	result := make(ast.InlineSlice, 0, 1)

	state := 0 // 0=unknown,1=non-spaces,2=spaces
	lastPos := 0
	for pos, ch := range text {
		if input.IsSpace(ch) {
		if runes.IsSpace(ch) {
			if state == 1 {
				result = append(result, &ast.TextNode{Text: text[lastPos:pos]})
				lastPos = pos
			}
			state = 2
		} else {
			if state == 2 {
357
358
359
360
361
362
363
364

365
366
367
368
369
370
371
357
358
359
360
361
362
363

364
365
366
367
368
369
370
371







-
+







	}
	return sb.String()
}

func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice {
	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:  ast.LiteralProg,
			Code:  ast.LiteralProg,
			Attrs: nil, //TODO
			Text:  cleanCodeSpan(string(node.Text(p.source))),
		},
	}
}

func cleanCodeSpan(text string) string {
387
388
389
390
391
392
393
394

395
396

397
398
399
400

401
402
403
404
405
406
407
387
388
389
390
391
392
393

394
395

396
397
398
399

400
401
402
403
404
405
406
407







-
+

-
+



-
+







		return text
	}
	sb.WriteString(text[lastPos:])
	return sb.String()
}

func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice {
	kind := ast.FormatEmph
	code := ast.FormatEmph
	if node.Level == 2 {
		kind = ast.FormatStrong
		code = ast.FormatStrong
	}
	return ast.InlineSlice{
		&ast.FormatNode{
			Kind:    kind,
			Code:    code,
			Attrs:   nil, //TODO
			Inlines: p.acceptInlineSlice(node),
		},
	}
}

func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice {
478
479
480
481
482
483
484
485

486
487
488
489
490
478
479
480
481
482
483
484

485
486
487
488
489
490







-
+





	segs := make([]string, 0, node.Segments.Len())
	for i := 0; i < node.Segments.Len(); i++ {
		segment := node.Segments.At(i)
		segs = append(segs, string(segment.Value(p.source)))
	}
	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:  ast.LiteralHTML,
			Code:  ast.LiteralHTML,
			Attrs: nil, // TODO: add HTML as language
			Text:  strings.Join(segs, ""),
		},
	}
}

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

-
+



















-







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

35
36
37
38
39
40
41
42
43
44
45






46
47
48
49
50
51
52
35
36
37
38
39
40
41




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







-
-
-
-
+
+
+
+
+
+







	}
	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.ParaNode{
				Inlines: convertToInlineSlice(value, meta.Type(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 {
81
82
83
84
85
86
87
88

89
90
91
92
93
94
95
83
84
85
86
87
88
89

90
91
92
93
94
95
96
97







-
+







	return result
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{
		&ast.FormatNode{
			Kind:  ast.FormatSpan,
			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"},

Changes to parser/plain/plain.go.

14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

91
92
93
94
95
96
97
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30





31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48






49
50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70






71
72
73

74
75
76
77
78
79
80
81







+









-
-
-
-
-


















-
-
-
-
-
-


-
+



















-
-
-
-
-
-



-
+







import (
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/runes"
)

func init() {
	parser.Register(&parser.Info{
		Name:         "txt",
		AltNames:     []string{"plain", "text"},
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
	})
	parser.Register(&parser.Info{
		Name:         "html",
		ParseBlocks:  parseBlocksHTML,
		ParseInlines: parseInlinesHTML,
	})
	parser.Register(&parser.Info{
		Name:         "css",
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
	})
	parser.Register(&parser.Info{
		Name:         "svg",
		ParseBlocks:  parseSVGBlocks,
		ParseInlines: parseSVGInlines,
	})
	parser.Register(&parser.Info{
		Name:         "mustache",
		ParseBlocks:  parseBlocks,
		ParseInlines: parseInlines,
	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
	return doParseBlocks(inp, m, syntax, ast.VerbatimProg)
}
func parseBlocksHTML(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
	return doParseBlocks(inp, m, syntax, ast.VerbatimHTML)
}
func doParseBlocks(inp *input.Input, m *meta.Meta, syntax string, kind ast.VerbatimKind) ast.BlockSlice {
	return ast.BlockSlice{
		&ast.VerbatimNode{
			Kind:  kind,
			Code:  ast.VerbatimProg,
			Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}},
			Lines: readLines(inp),
		},
	}
}

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 doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:  kind,
			Code:  ast.LiteralProg,
			Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}},
			Text:  inp.Src[0:inp.Pos],
		},
	}
}

func parseSVGBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
116
117
118
119
120
121
122
123

124
125
126
127
128
129
130
131
132
100
101
102
103
104
105
106

107
108
109
110
111
112
113
114
115
116







-
+









			Blob:   []byte(svgSrc),
			Syntax: syntax,
		},
	}
}

func scanSVG(inp *input.Input) string {
	for input.IsSpace(inp.Ch) {
	for runes.IsSpace(inp.Ch) {
		inp.Next()
	}
	svgSrc := inp.Src[inp.Pos:]
	if !strings.HasPrefix(svgSrc, "<svg ") {
		return ""
	}
	// TODO: check proper end </svg>
	return svgSrc
}

Changes to parser/zettelmark/block.go.

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







-
+


-
+

-
+



-
+


















-
+









-
+












-
+







		return nil, false
	}
	attrs := cp.parseAttributes(true)
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	var kind ast.VerbatimKind
	var code ast.VerbatimCode
	switch fch {
	case '`', runeModGrave:
		kind = ast.VerbatimProg
		code = ast.VerbatimProg
	case '%':
		kind = ast.VerbatimComment
		code = ast.VerbatimComment
	default:
		panic(fmt.Sprintf("%q is not a verbatim char", fch))
	}
	rn = &ast.VerbatimNode{Kind: kind, Attrs: attrs}
	rn = &ast.VerbatimNode{Code: code, Attrs: attrs}
	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()
		rn.Lines = append(rn.Lines, inp.Src[posL:inp.Pos])
	}
}

var runeRegion = map[rune]ast.RegionKind{
var runeRegion = map[rune]ast.RegionCode{
	':': 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
	kind, ok := runeRegion[fch]
	code, 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)
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	rn = &ast.RegionNode{Kind: kind, Attrs: attrs}
	rn = &ast.RegionNode{Code: code, Attrs: attrs}
	var lastPara *ast.ParaNode
	inp.EatEOL()
	for {
		posL := inp.Pos
		switch inp.Ch {
		case fch:
			if cp.countDelim(fch) >= cnt {
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
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







-
+








-
-
+
+



-
+



-
-
+
+

-
+
-
-
-
-
-
+



-
+

-
+


















-
-
+
+

-
-
+
+







-
+







		return nil, false
	}
	attrs := cp.parseAttributes(true)
	inp.SkipToEOL()
	return &ast.HRuleNode{Attrs: attrs}, true
}

var mapRuneNestedList = map[rune]ast.NestedListKind{
var mapRuneNestedList = map[rune]ast.NestedListCode{
	'*': ast.NestedListUnordered,
	'#': ast.NestedListOrdered,
	'>': ast.NestedListQuote,
}

// parseNestedList parses a list.
func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) {
	inp := cp.inp
	kinds := cp.parseNestedListKinds()
	if kinds == nil {
	codes := cp.parseNestedListCodes()
	if codes == nil {
		return nil, false
	}
	cp.skipSpace()
	if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) {
	if codes[len(codes)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) {
		return nil, false
	}

	if len(kinds) < len(cp.lists) {
		cp.lists = cp.lists[:len(kinds)]
	if len(codes) < len(cp.lists) {
		cp.lists = cp.lists[:len(codes)]
	}
	ln, newLnCount := cp.buildNestedList(kinds)
	ln, newLnCount := cp.buildNestedList(codes)
	pn := cp.parseLinePara()
	if pn == nil {
		pn = &ast.ParaNode{}
	}
	ln.Items = append(ln.Items, ast.ItemSlice{pn})
	ln.Items = append(ln.Items, ast.ItemSlice{cp.parseLinePara()})
	return cp.cleanupParsedNestedList(newLnCount)
}

func (cp *zmkP) parseNestedListKinds() []ast.NestedListKind {
func (cp *zmkP) parseNestedListCodes() []ast.NestedListCode {
	inp := cp.inp
	codes := make([]ast.NestedListKind, 0, 4)
	codes := make([]ast.NestedListCode, 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(kinds []ast.NestedListKind) (ln *ast.NestedListNode, newLnCount int) {
	for i, kind := range kinds {
func (cp *zmkP) buildNestedList(codes []ast.NestedListCode) (ln *ast.NestedListNode, newLnCount int) {
	for i, code := range codes {
		if i < len(cp.lists) {
			if cp.lists[i].Kind != kind {
				ln = &ast.NestedListNode{Kind: kind}
			if cp.lists[i].Code != code {
				ln = &ast.NestedListNode{Code: code}
				newLnCount++
				cp.lists[i] = ln
				cp.lists = cp.lists[:i+1]
			} else {
				ln = cp.lists[i]
			}
		} else {
			ln = &ast.NestedListNode{Kind: kind}
			ln = &ast.NestedListNode{Code: code}
			newLnCount++
			cp.lists = append(cp.lists, ln)
		}
	}
	return ln, newLnCount
}

484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
480
481
482
483
484
485
486



487
488
489
490
491
492
493







-
-
-







	}
	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

Changes to parser/zettelmark/inline.go.

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







-
-
+
+
+
-
-
-
-
-
+

-

+


-
-


+







	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])
		for {
		sepPos := inp.Pos
		inp.SetPos(pos)
		for inp.Pos < sepPos {
			in := cp.parseInline()
			if in == nil {
				break
			}
			ins = append(ins, in)
			ins = append(ins, cp.parseInline())
		}
		cp.inp = inp
		inp.Next()
		pos = inp.Pos
	} else if hasSpace {
		return "", nil, false
	} else {
		inp.SetPos(pos)
	}

	inp.SetPos(pos)
	cp.skipSpace()
	pos = inp.Pos
	if !cp.readReferenceToClose(closeCh) {
		return "", nil, false
	}
	ref = inp.Src[pos:inp.Pos]
	inp.Next()
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
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







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

















-
-
-
-
-
-







		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
	for {
		switch inp.Ch {
		case input.EOS, '\n', '\r', ' ':
			return false
		case '\\':
			inp.Next()
			switch inp.Ch {
			case input.EOS, '\n', '\r':
				return false
			}
		case closeCh:
			return true
		}
		inp.Next()
	}
}

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







-
+





-
+
















-
+








-
+







	for inp.Ch == '%' {
		inp.Next()
	}
	cp.skipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return &ast.LiteralNode{Kind: ast.LiteralComment, Text: inp.Src[pos:inp.Pos]}, true
			return &ast.LiteralNode{Code: ast.LiteralComment, Text: inp.Src[pos:inp.Pos]}, true
		}
		inp.Next()
	}
}

var mapRuneFormat = map[rune]ast.FormatKind{
var mapRuneFormat = map[rune]ast.FormatCode{
	'/':  ast.FormatItalic,
	'*':  ast.FormatBold,
	'_':  ast.FormatUnder,
	'~':  ast.FormatStrike,
	'\'': ast.FormatMonospace,
	'^':  ast.FormatSuper,
	',':  ast.FormatSub,
	'<':  ast.FormatQuotation,
	'"':  ast.FormatQuote,
	';':  ast.FormatSmall,
	':':  ast.FormatSpan,
}

func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	kind, ok := mapRuneFormat[fch]
	code, 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{Kind: kind}
	fn := &ast.FormatNode{Code: code}
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			inp.Next()
			if inp.Ch == fch {
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
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







-
+









-
+







-
+







				return nil, false
			}
			fn.Inlines = append(fn.Inlines, in)
		}
	}
}

var mapRuneLiteral = map[rune]ast.LiteralKind{
var mapRuneLiteral = map[rune]ast.LiteralCode{
	'`':          ast.LiteralProg,
	runeModGrave: ast.LiteralProg,
	'+':          ast.LiteralKeyb,
	'=':          ast.LiteralOutput,
}

func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	kind, ok := mapRuneLiteral[fch]
	code, 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{Kind: kind}
	fn := &ast.LiteralNode{Code: code}
	inp.Next()
	var sb strings.Builder
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {

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



1
2
3
4
5
6
7
8
9
10
11
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-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 zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
import "zettelstore.de/z/ast"
	"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.

30
31
32
33
34
35
36

37
38


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79






























































80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
30
31
32
33
34
35
36
37


38
39









































40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101


















102
103
104
105
106
107
108







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







}

// postProcessor is a visitor that cleans the abstract syntax tree.
type postProcessor struct {
	inVerse bool
}

// VisitPara post-processes a paragraph.
func (pp *postProcessor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
func (pp *postProcessor) VisitPara(pn *ast.ParaNode) {
	if pn != nil {
	case *ast.ParaNode:
		n.Inlines = pp.processInlineSlice(n.Inlines)
	case *ast.RegionNode:
		oldVerse := pp.inVerse
		if n.Kind == ast.RegionVerse {
			pp.inVerse = true
		}
		n.Blocks = pp.processBlockSlice(n.Blocks)
		pp.inVerse = oldVerse
		n.Inlines = pp.processInlineSlice(n.Inlines)
	case *ast.HeadingNode:
		n.Inlines = pp.processInlineSlice(n.Inlines)
	case *ast.NestedListNode:
		for i, item := range n.Items {
			n.Items[i] = pp.processItemSlice(item)
		}
	case *ast.DescriptionListNode:
		for i, def := range n.Descriptions {
			n.Descriptions[i].Term = pp.processInlineSlice(def.Term)
			for j, b := range def.Descriptions {
				n.Descriptions[i].Descriptions[j] = pp.processDescriptionSlice(b)
			}
		}
	case *ast.TableNode:
		width := tableWidth(n)
		n.Align = make([]ast.Alignment, width)
		for i := 0; i < width; i++ {
			n.Align[i] = ast.AlignDefault
		}
		if len(n.Rows) > 0 && isHeaderRow(n.Rows[0]) {
			n.Header = n.Rows[0]
			n.Rows = n.Rows[1:]
			pp.visitTableHeader(n)
		}
		if len(n.Header) > 0 {
			n.Header = appendCells(n.Header, width, n.Align)
			for i, cell := range n.Header {
				pp.processCell(cell, n.Align[i])
			}
		}
		pp.visitTableRows(n, width)
		pn.Inlines = pp.processInlineSlice(pn.Inlines)
	}
}

// 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) {
	oldVerse := pp.inVerse
	if rn.Code == ast.RegionVerse {
		pp.inVerse = true
	}
	rn.Blocks = pp.processBlockSlice(rn.Blocks)
	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) VisitHRule(hn *ast.HRuleNode) {}

// VisitList post-processes a list.
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 i, def := range dn.Descriptions {
		dn.Descriptions[i].Term = pp.processInlineSlice(def.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) {
	width := tableWidth(tn)
	tn.Align = make([]ast.Alignment, width)
	for i := 0; i < width; i++ {
		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)
	case *ast.LinkNode:
		n.Inlines = pp.processInlineSlice(n.Inlines)
	case *ast.ImageNode:
		n.Inlines = pp.processInlineSlice(n.Inlines)
	case *ast.CiteNode:
		n.Inlines = pp.processInlineSlice(n.Inlines)
	case *ast.FootnoteNode:
		n.Inlines = pp.processInlineSlice(n.Inlines)
	case *ast.FormatNode:
		if n.Attrs != nil && n.Attrs.HasDefault() {
			if newKind, ok := mapSemantic[n.Kind]; ok {
				n.Attrs.RemoveDefault()
				n.Kind = newKind
			}
		}
		n.Inlines = pp.processInlineSlice(n.Inlines)
	}
	return nil
}

func (pp *postProcessor) visitTableHeader(tn *ast.TableNode) {
	for pos, cell := range tn.Header {
		inlines := cell.Inlines
		if len(inlines) == 0 {
			continue
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
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







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






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



-
-
+
+

-







		}
	} else {
		cell.Align = colAlign
	}
	cell.Inlines = pp.processInlineSlice(cell.Inlines)
}

// VisitBLOB does nothing.
func (pp *postProcessor) VisitBLOB(bn *ast.BLOBNode) {}

// VisitText does nothing.
func (pp *postProcessor) VisitText(tn *ast.TextNode) {}

// VisitTag does nothing.
func (pp *postProcessor) VisitTag(tn *ast.TagNode) {}

// VisitSpace does nothing.
func (pp *postProcessor) VisitSpace(sn *ast.SpaceNode) {}

// VisitBreak does nothing.
func (pp *postProcessor) VisitBreak(bn *ast.BreakNode) {}

// VisitLink post-processes a link.
func (pp *postProcessor) VisitLink(ln *ast.LinkNode) {
	ln.Inlines = pp.processInlineSlice(ln.Inlines)
}

// VisitImage post-processes an image.
func (pp *postProcessor) VisitImage(in *ast.ImageNode) {
	if len(in.Inlines) > 0 {
		in.Inlines = pp.processInlineSlice(in.Inlines)
	}
}

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

var mapSemantic = map[ast.FormatKind]ast.FormatKind{
var mapSemantic = map[ast.FormatCode]ast.FormatCode{
	ast.FormatItalic: ast.FormatEmph,
	ast.FormatBold:   ast.FormatStrong,
	ast.FormatUnder:  ast.FormatInsert,
	ast.FormatStrike: ast.FormatDelete,
}

// 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
		}
	}
	fn.Inlines = pp.processInlineSlice(fn.Inlines)
}

// 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 {
	if len(bns) == 0 {
		return nil
	for _, bn := range bns {
		bn.Accept(pp)
	}
	ast.WalkBlockSlice(pp, bns)
	fromPos, toPos := 0, 0
	for fromPos < len(bns) {
		bns[toPos] = bns[fromPos]
		fromPos++
		switch bn := bns[toPos].(type) {
		case *ast.ParaNode:
			if len(bn.Inlines) > 0 {
224
225
226
227
228
229
230
231
232
233
234
235

236
237
238
239
240
241
242
281
282
283
284
285
286
287



288

289
290
291
292
293
294
295
296







-
-
-

-
+







	}
	return bns[: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 {
		ast.Walk(pp, in)
		in.Accept(pp)
	}
	fromPos, toPos := 0, 0
	for fromPos < len(ins) {
		ins[toPos] = ins[fromPos]
		fromPos++
		switch in := ins[toPos].(type) {
		case *ast.ParaNode:
254
255
256
257
258
259
260
261
262
263
264
265

266
267
268
269
270
271
272
308
309
310
311
312
313
314



315

316
317
318
319
320
321
322
323







-
-
-

-
+







	}
	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 {
		ast.Walk(pp, dn)
		dn.Accept(pp)
	}
	fromPos, toPos := 0, 0
	for fromPos < len(dns) {
		dns[toPos] = dns[fromPos]
		fromPos++
		switch dn := dns[toPos].(type) {
		case *ast.ParaNode:
286
287
288
289
290
291
292
293



294
295
296
297
298
299
300
337
338
339
340
341
342
343

344
345
346
347
348
349
350
351
352
353







-
+
+
+








// 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
	}
	ast.WalkInlineSlice(pp, ins)
	for _, in := range ins {
		in.Accept(pp)
	}

	if !pp.inVerse {
		ins = processInlineSliceHead(ins)
	}
	toPos := pp.processInlineSliceCopy(ins)
	toPos = pp.processInlineSliceTail(ins, toPos)
	ins = ins[:toPos:toPos]

Changes to parser/zettelmark/zettelmark_test.go.

46
47
48
49
50
51
52
53

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
46
47
48
49
50
51
52

53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71
72

73
74
75
76
77
78
79







-
+









-










-








	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)
			var tv TestVisitor
			ast.WalkBlockSlice(&tv, bns)
			tv.visitBlockSlice(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", ""},
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
95
96
97
98
99
100
101

102
103
104
105
106
107
108
109

110
111
112
113
114
115
116
117
118

119
120
121
122
123
124
125
126
127

128
129
130
131
132
133
134







-








-









-









-







		{"...?", "(PARA \u2026?)"},
		{"...-", "(PARA ...-)"},
		{"a...b", "(PARA a...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 [[]])"},
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
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







-
-
-
-
-
-
-
-
-




-



















-













-







		{"[[a]]go", "(PARA (LINK a a) go)"},
		{"[[a]]{go}", "(PARA (LINK a a)[ATTR go])"},
		{"[[[[a]]|b]]", "(PARA (LINK [[a [[a) |b]])"},
		{"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"},
		{"[[[b]c|d]]", "(PARA (LINK d [b]c))"},
		{"[[a[]c|d]]", "(PARA (LINK d a[]c))"},
		{"[[a[b]|d]]", "(PARA (LINK d a[b]))"},
		{"[[\\|]]", "(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|http://a]]", "(PARA (LINK http://a http://a))"},
	})
}

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) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"{", "(PARA {)"},
		{"{{", "(PARA {{)"},
		{"{{|", "(PARA {{|)"},
		{"{{}", "(PARA {{})"},
		{"{{|}", "(PARA {{|})"},
		{"{{}}", "(PARA {{}})"},
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
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







-
-
-
-
-
-
-
-
-




-












-















-




















-







		{"{{b|a}}", "(PARA (IMAGE a b))"},
		{"{{b| a}}", "(PARA (IMAGE 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}})"},
		{"{{\\|}}", "(PARA (IMAGE %5C%7C))"},
		{"{{\\||a}}", "(PARA (IMAGE a |))"},
		{"{{b\\||a}}", "(PARA (IMAGE a b|))"},
		{"{{b\\|c|a}}", "(PARA (IMAGE a b|c))"},
		{"{{\\}}}", "(PARA (IMAGE %5C%7D))"},
		{"{{\\}|a}}", "(PARA (IMAGE a }))"},
		{"{{b\\}|a}}", "(PARA (IMAGE a b}))"},
		{"{{\\}\\||a}}", "(PARA (IMAGE a }|))"},
		{"{{http://a|http://a}}", "(PARA (IMAGE http://a http://a))"},
	})
}

func TestTag(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"#", "(PARA #)"},
		{"##", "(PARA ##)"},
		{"###", "(PARA ###)"},
		{"#tag", "(PARA #tag#)"},
		{"#tag,", "(PARA #tag# ,)"},
		{"#t-g ", "(PARA #t-g#)"},
		{"#t_g", "(PARA #t_g#)"},
	})
}

func TestMark(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[!", "(PARA [!)"},
		{"[!\n", "(PARA [!)"},
		{"[!]", "(PARA (MARK *))"},
		{"[! ]", "(PARA [! SP ])"},
		{"[!a]", "(PARA (MARK a))"},
		{"[!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))"},
	})
}

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})"},
		{" %%b", "(PARA {% b})"},
		{"%%b ", "(PARA {% b })"},
		{"100%", "(PARA 100%)"},
	})
}

func TestFormat(t *testing.T) {
	t.Parallel()
	for _, ch := range []string{"/", "*", "_", "~", "'", "^", ",", "<", "\"", ";", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
			{"$$a$$", "(PARA {$ a})"},
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
294
295
296
297
298
299
300

301
302
303
304
305
306
307







-







		{"//****//", "(PARA {/ {*}})"},
		{"//**a**//", "(PARA {/ {* a}})"},
		{"//**//**", "(PARA // {* //})"},
	})
}

func TestLiteral(t *testing.T) {
	t.Parallel()
	for _, ch := range []string{"`", "+", "="} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
			{"$$a$$", "(PARA {$ a})"},
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
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







-










-







-



















-











-












-












-













-







		{"++``a``++", "(PARA {+ ``a``})"},
		{"++``++``", "(PARA {+ ``} ``)"},
		{"++\\+++", "(PARA {+ +})"},
	})
}

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})"},
		{"\"\"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# ;)"},
		{"&1;", "(PARA &1;)"},
		// Good cases
		{"&lt;", "(PARA <)"},
		{"&#48;", "(PARA 0)"},
		{"&#x4A;", "(PARA J)"},
		{"&#X4a;", "(PARA J)"},
		{"&hellip;", "(PARA \u2026)"},
		{"E: &amp;,&#13;;&#xa;.", "(PARA E: SP &,\r;\n.)"},
	})
}

func TestVerbatim(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 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)"},
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
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







-














-







		{" =", "(PARA =)"},
		{"=== h\na", "(H2 h)(PARA a)"},
		{"=== h i {-}", "(H2 h SP i)[ATTR -]"},
	})
}

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 $)"},
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
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







-
+





-







-








		// 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 may have empty items
		// Quotation lists mayx have empty items
		{">", "(QL {})"},
	})
}

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







-













-







		{"; 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))"},
		{"|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)))"},
	})
}

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]"},
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
567
568
569
570
571
572
573

574
575
576
577
578
579
580







-







		{":::{.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])"},
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
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







-













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



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





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




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








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













+
+
+
+
+
+
+
-
+






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



-
+


-








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

func (tv *TestVisitor) String() string { return tv.b.String() }

func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor {
func (tv *TestVisitor) VisitPara(pn *ast.ParaNode) {
	switch n := node.(type) {
	case *ast.ParaNode:
		tv.b.WriteString("(PARA")
	tv.b.WriteString("(PARA")
		tv.visitInlineSlice(n.Inlines)
		tv.b.WriteByte(')')
	case *ast.VerbatimNode:
		code, ok := mapVerbatimKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind))
		}
		tv.b.WriteString(code)
		for _, line := range n.Lines {
			tv.b.WriteByte('\n')
			tv.b.WriteString(line)
		}
		tv.b.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.b.WriteString(code)
		if n.Blocks != nil {
			tv.b.WriteByte(' ')
			ast.WalkBlockSlice(tv, n.Blocks)
		}
		if len(n.Inlines) > 0 {
			tv.b.WriteString(" (LINE")
			tv.visitInlineSlice(n.Inlines)
			tv.b.WriteByte(')')
		}
		tv.b.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HeadingNode:
		fmt.Fprintf(&tv.b, "(H%d", n.Level)
		tv.visitInlineSlice(n.Inlines)
		tv.b.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HRuleNode:
		tv.b.WriteString("(HR)")
		tv.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		tv.b.WriteString(mapNestedListKind[n.Kind])
		for _, item := range n.Items {
			tv.b.WriteString(" {")
			ast.WalkItemSlice(tv, item)
			tv.b.WriteByte('}')
		}
		tv.b.WriteByte(')')
	case *ast.DescriptionListNode:
		tv.b.WriteString("(DL")
		for _, def := range n.Descriptions {
			tv.b.WriteString(" (DT")
			tv.visitInlineSlice(def.Term)
			tv.b.WriteByte(')')
			for _, b := range def.Descriptions {
				tv.b.WriteString(" (DD ")
				ast.WalkDescriptionSlice(tv, b)
				tv.b.WriteByte(')')
			}
		}
		tv.b.WriteByte(')')
	case *ast.TableNode:
		tv.b.WriteString("(TAB")
		if len(n.Header) > 0 {
			tv.b.WriteString(" (TR")
			for _, cell := range n.Header {
				tv.b.WriteString(" (TH")
				tv.b.WriteString(alignString[cell.Align])
				tv.visitInlineSlice(cell.Inlines)
	tv.visitInlineSlice(pn.Inlines)
				tv.b.WriteString(")")
			}
			tv.b.WriteString(")")
		}
		if len(n.Rows) > 0 {
			tv.b.WriteString(" ")
			for _, row := range n.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(")")
	case *ast.BLOBNode:
		tv.b.WriteString("(BLOB ")
		tv.b.WriteString(n.Syntax)
		tv.b.WriteString(")")
	case *ast.TextNode:
		tv.b.WriteString(n.Text)
	case *ast.TagNode:
		tv.b.WriteByte('#')
		tv.b.WriteString(n.Tag)
		tv.b.WriteByte('#')
	case *ast.SpaceNode:
		if len(n.Lexeme) == 1 {
			tv.b.WriteString("SP")
		} else {
			fmt.Fprintf(&tv.b, "SP%d", len(n.Lexeme))
		}
	case *ast.BreakNode:
		if n.Hard {
			tv.b.WriteString("HB")
		} else {
			tv.b.WriteString("SB")
		}
	case *ast.LinkNode:
		fmt.Fprintf(&tv.b, "(LINK %v", n.Ref)
		tv.visitInlineSlice(n.Inlines)
		tv.b.WriteByte(')')
	tv.b.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.ImageNode:
		fmt.Fprintf(&tv.b, "(IMAGE %v", n.Ref)
		tv.visitInlineSlice(n.Inlines)
		tv.b.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.CiteNode:
		fmt.Fprintf(&tv.b, "(CITE %s", n.Key)
		tv.visitInlineSlice(n.Inlines)
		tv.b.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.FootnoteNode:
		tv.b.WriteString("(FN")
		tv.visitInlineSlice(n.Inlines)
		tv.b.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		tv.b.WriteString("(MARK")
		if len(n.Text) > 0 {
			tv.b.WriteByte(' ')
			tv.b.WriteString(n.Text)
		}
}
		tv.b.WriteByte(')')
	case *ast.FormatNode:
		fmt.Fprintf(&tv.b, "{%c", mapFormatKind[n.Kind])
		tv.visitInlineSlice(n.Inlines)
		tv.b.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.b.WriteByte('{')
		tv.b.WriteRune(code)
		if len(n.Text) > 0 {
			tv.b.WriteByte(' ')
			tv.b.WriteString(n.Text)
		}
		tv.b.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	}
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
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 mapRegionKind = map[ast.RegionKind]string{
var mapRegionCode = map[ast.RegionCode]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 mapNestedListKind = map[ast.NestedListKind]string{
var mapNestedListCode = map[ast.NestedListCode]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(")")
		}
		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(")")
}

func (tv *TestVisitor) VisitBLOB(bn *ast.BLOBNode) {
	tv.b.WriteString("(BLOB ")
	tv.b.WriteString(bn.Syntax)
	tv.b.WriteString(")")
}

func (tv *TestVisitor) VisitText(tn *ast.TextNode) {
	tv.b.WriteString(tn.Text)
}
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) {
	if len(sn.Lexeme) == 1 {
		tv.b.WriteString("SP")
	} else {
		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.visitAttributes(tn.Attrs)
}
func (tv *TestVisitor) VisitImage(in *ast.ImageNode) {
	fmt.Fprintf(&tv.b, "(IMAGE %s", in.Ref)
	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) {
	tv.b.WriteString("(MARK")
	if len(mn.Text) > 0 {
		tv.b.WriteByte(' ')
		tv.b.WriteString(mn.Text)
	}
	tv.b.WriteByte(')')
}

var mapFormatKind = map[ast.FormatKind]rune{
var mapCode = map[ast.FormatCode]rune{
	ast.FormatItalic:    '/',
	ast.FormatBold:      '*',
	ast.FormatUnder:     '_',
	ast.FormatStrike:    '~',
	ast.FormatMonospace: '\'',
	ast.FormatSuper:     '^',
	ast.FormatSub:       ',',
	ast.FormatQuote:     '"',
	ast.FormatQuotation: '<',
	ast.FormatSmall:     ';',
	ast.FormatSpan:      ':',
}

func (tv *TestVisitor) VisitFormat(fn *ast.FormatNode) {
	fmt.Fprintf(&tv.b, "{%c", mapCode[fn.Code])
	tv.visitInlineSlice(fn.Inlines)
	tv.b.WriteByte('}')
	tv.visitAttributes(fn.Attrs)
}

var mapLiteralKind = map[ast.LiteralKind]rune{
var mapLiteralCode = map[ast.LiteralCode]rune{
	ast.LiteralProg:    '`',
	ast.LiteralKeyb:    '+',
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
}

func (tv *TestVisitor) VisitLiteral(ln *ast.LiteralNode) {
	code, ok := mapLiteralCode[ln.Code]
	if !ok {
		panic(fmt.Sprintf("No element for code %v", ln.Code))
	}
	tv.b.WriteByte('{')
	tv.b.WriteRune(code)
	if len(ln.Text) > 0 {
		tv.b.WriteByte(' ')
		tv.b.WriteString(ln.Text)
	}
	tv.b.WriteByte('}')
	tv.visitAttributes(ln.Attrs)
}
func (tv *TestVisitor) visitBlockSlice(bns ast.BlockSlice) {
	for _, bn := range bns {
		bn.Accept(tv)
	}
}
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(' ')
		ast.Walk(tv, in)
		in.Accept(tv)
	}
}

func (tv *TestVisitor) visitAttributes(a *ast.Attributes) {
	if a == nil || len(a.Attrs) == 0 {
		return
	}
	tv.b.WriteString("[ATTR")

	keys := make([]string, 0, len(a.Attrs))

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

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

Added 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

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

Added 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

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

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

Added place/constplace/emoji_spin.gif.

cannot compute difference between binary files

Added place/constplace/error.mustache.







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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Added 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 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/compile.go.

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

func compileSearch(searcher Searcher, poss, negs []expValue) MetaMatchFunc {
	if len(poss) == 0 {
		if len(negs) == 0 {
			return nil
		}
		return makeNegOnlySearch(searcher, negs)
	}
	if len(negs) == 0 {
		return makePosOnlySearch(searcher, poss)
	}
	return makePosNegSearch(searcher, poss, negs)
}

func makePosOnlySearch(searcher Searcher, poss []expValue) MetaMatchFunc {
	retrievePos := compileRetrieveZids(searcher, poss)
	var ids id.Set
	return func(m *meta.Meta) bool {
		if ids == nil {
			ids = retrievePos()
		}
		_, ok := ids[m.Zid]
		return ok
	}
}

func makeNegOnlySearch(searcher Searcher, negs []expValue) MetaMatchFunc {
	retrieveNeg := compileRetrieveZids(searcher, negs)
	var ids id.Set
	return func(m *meta.Meta) bool {
		if ids == nil {
			ids = retrieveNeg()
		}
		_, ok := ids[m.Zid]
		return !ok
	}
}

func makePosNegSearch(searcher Searcher, poss, negs []expValue) MetaMatchFunc {
	retrievePos := compileRetrieveZids(searcher, poss)
	retrieveNeg := compileRetrieveZids(searcher, 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(searcher Searcher, 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(searcher, 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(searcher Searcher, op compareOp) selectorFunc {
	switch op {
	case cmpDefault, cmpContains:
		return searcher.SearchContains
	case cmpEqual:
		return searcher.SearchEqual
	case cmpPrefix:
		return searcher.SearchPrefix
	case cmpSuffix:
		return searcher.SearchSuffix
	default:
		panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op))
	}
}

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

Changes to search/print.go.

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30

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

44
45
46
47
48
49
50
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29

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

43
44
45
46
47
48
49
50







-
+







-
+












-
+







	"io"
	"sort"
	"strconv"

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

// Print the search to a writer.
// 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")
		printSelectExprValues(w, s.search)
		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)
		printSelectExprValues(w, s.tags[name])
		printFilterExprValues(w, s.tags[name])
		space = true
	}
	if s.negate {
		io.WriteString(w, ")")
		space = true
	}

72
73
74
75
76
77
78
79

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

79
80
81
82
83
84
85
86







-
+







	if lim := s.limit; lim > 0 {
		_ = printSpace(w, space)
		io.WriteString(w, "LIMIT ")
		io.WriteString(w, strconv.Itoa(lim))
	}
}

func printSelectExprValues(w io.Writer, values []expValue) {
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 {

Changes to search/search.go.

17
18
19
20
21
22
23
24
25


26
27
28

29
30
31
32

33
34
35
36

37
38
39
40

41
42
43

44
45
46
47
48
49
50

51
52
53
54

55
56
57
58
59
60
61
17
18
19
20
21
22
23


24
25
26
27

28
29
30
31

32
33
34
35

36
37
38
39

40
41
42

43
44
45
46
47
48
49

50
51
52
53

54
55
56
57
58
59
60
61







-
-
+
+


-
+



-
+



-
+



-
+


-
+






-
+



-
+







	"strings"
	"sync"

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

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {
// 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.
	SearchEqual(word string) id.Set
	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.
	SearchPrefix(prefix string) id.Set
	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.
	SearchSuffix(suffix string) id.Set
	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.
	SearchContains(s string) id.Set
	SelectContains(s string) id.Set
}

// MetaMatchFunc is a function determine whethe some metadata should be selected or not.
// 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 selecting
	// 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 selecting process
	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
}
77
78
79
80
81
82
83
84

85
86
87
88
89
90
91
77
78
79
80
81
82
83

84
85
86
87
88
89
90
91







-
+








type expValue struct {
	value  string
	op     compareOp
	negate bool
}

// AddExpr adds a match expression to the search.
// 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()
127
128
129
130
131
132
133
134

135
136
137
138
139
140
141
142
143
144
145

146
147
148
149
150
151
152
127
128
129
130
131
132
133

134
135
136
137
138
139
140
141
142
143
144

145
146
147
148
149
150
151
152







-
+










-
+







		return s[1:], negate, cmpSuffix
	case '~':
		return s[1:], negate, cmpContains
	}
	return s, negate, cmpDefault
}

// SetNegate changes the search to reverse its selection.
// 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-selection predicate.
// 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 {
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
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







-
+


















-
-
+
+

-
+




-
-
+
+






-
+







		return 0
	}
	s.mx.RLock()
	defer s.mx.RUnlock()
	return s.limit
}

// HasComputedMetaKey returns true, if the search references a metadata key which
// 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 select specification.
func (s *Search) CompileMatch(searcher Searcher) MetaMatchFunc {
// CompileMatch returns a function to match meta data based on filter specification.
func (s *Search) CompileMatch(selector Selector) MetaMatchFunc {
	if s == nil {
		return selectNone
		return filterNone
	}
	s.mx.Lock()
	defer s.mx.Unlock()

	compMeta := compileSelect(s.tags)
	compSearch := compileFullSearch(searcher, s.search)
	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 selectNone(m *meta.Meta) bool { return true }
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 {
283
284
285
286
287
288
289
290

291
292
293
294
295
296
297
283
284
285
286
287
288
289

290
291
292
293
294
295
296
297







-
+








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 selectNone
			return filterNone
		}
		if negate {
			return func(m *meta.Meta) bool { return !compSearch(m) }
		}
		return compSearch
	}
	if compSearch == nil {

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

















































































































































































































































































































































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

// compileSelect calculates a selection func based on the given select criteria.
func compileSelect(tags expTagValues) MetaMatchFunc {
	posSpecs, negSpecs, nomatch := createSelectSpecs(tags)
	if len(posSpecs) > 0 || len(negSpecs) > 0 || len(nomatch) > 0 {
		return makeSearchMetaMatchFunc(posSpecs, negSpecs, nomatch)
	}
	return nil
}

func createSelectSpecs(tags map[string][]expValue) (posSpecs, negSpecs []matchSpec, nomatch []string) {
	posSpecs = make([]matchSpec, 0, len(tags))
	negSpecs = make([]matchSpec, 0, len(tags))
	for key, values := range tags {
		if !meta.KeyIsValid(key) {
			continue
		}
		if always, never := countEmptyValues(values); always+never > 0 {
			if never == 0 {
				posSpecs = append(posSpecs, matchSpec{key, matchAlways})
				continue
			}
			if always == 0 {
				negSpecs = append(negSpecs, matchSpec{key, nil})
				continue
			}
			// value must match always AND never, at the same time. This results in a no-match.
			nomatch = append(nomatch, key)
			continue
		}
		posMatch, negMatch := createPosNegMatchFunc(key, values)
		if posMatch != nil {
			posSpecs = append(posSpecs, matchSpec{key, posMatch})
		}
		if negMatch != nil {
			negSpecs = append(negSpecs, matchSpec{key, negMatch})
		}
	}
	return posSpecs, negSpecs, nomatch
}

func countEmptyValues(values []expValue) (always, never int) {
	for _, v := range values {
		if v.value != "" {
			continue
		}
		if v.negate {
			never++
		} else {
			always++
		}
	}
	return always, never
}

func createPosNegMatchFunc(key string, values []expValue) (posMatch, negMatch matchFunc) {
	posValues := make([]opValue, 0, len(values))
	negValues := make([]opValue, 0, len(values))
	for _, val := range values {
		if val.negate {
			negValues = append(negValues, opValue{value: val.value, op: val.op})
		} else {
			posValues = append(posValues, opValue{value: val.value, op: val.op})
		}
	}
	return createMatchFunc(key, posValues), createMatchFunc(key, negValues)
}

// opValue is an expValue, but w/o the field "negate"
type opValue struct {
	value string
	op    compareOp
}

func createMatchFunc(key string, values []opValue) matchFunc {
	if len(values) == 0 {
		return nil
	}
	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 []opValue) matchFunc {
	preValues := make([]bool, 0, len(values))
	for _, v := range values {
		preValues = append(preValues, meta.BoolValue(v.value))
	}
	return func(value string) bool {
		bValue := meta.BoolValue(value)
		for _, v := range preValues {
			if bValue != v {
				return false
			}
		}
		return true
	}
}

func createMatchIDFunc(values []opValue) matchFunc {
	return func(value string) bool {
		for _, v := range values {
			if !strings.HasPrefix(value, v.value) {
				return false
			}
		}
		return true
	}
}

func createMatchIDSetFunc(values []opValue) 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) {
					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 []opValue) 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) {
					return false
				}
			}
		}
		return true
	}
}

type tagQueryValue struct {
	value string
	equal bool // not equal == prefix
}

func processTagSet(valueSet [][]opValue) [][]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, equal: true}
			} else {
				tags[j] = tagQueryValue{value: tval, 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 []opValue) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if value != v.value {
				return false
			}
		}
		return true
	}
}

func createMatchWordSetFunc(values []opValue) 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) {
					return false
				}
			}
		}
		return true
	}
}

func createMatchStringFunc(values []opValue) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if !strings.Contains(value, v.value) {
				return false
			}
		}
		return true
	}
}

func makeSearchMetaMatchFunc(posSpecs, negSpecs []matchSpec, nomatch []string) MetaMatchFunc {
	return func(m *meta.Meta) bool {
		for _, key := range nomatch {
			if _, ok := m.Get(key); ok {
				return false
			}
		}
		for _, s := range posSpecs {
			if value, ok := m.Get(s.key); !ok || !s.match(value) {
				return false
			}
		}
		for _, s := range negSpecs {
			if s.match == nil {
				if _, ok := m.Get(s.key); ok {
					return false
				}
			} else if value, ok := m.Get(s.key); !ok || s.match(value) {
				return false
			}
		}
		return true
	}
}

func sliceToLower(sl []opValue) []opValue {
	result := make([]opValue, 0, len(sl))
	for _, s := range sl {
		result = append(result, opValue{
			value: strings.ToLower(s.value),
			op:    s.op,
		})
	}
	return result
}

func preprocessSet(set []opValue) [][]opValue {
	result := make([][]opValue, 0, len(set))
	for _, elem := range set {
		splitElems := strings.Split(elem.value, ",")
		valueElems := make([]opValue, 0, len(splitElems))
		for _, se := range splitElems {
			e := strings.TrimSpace(se)
			if len(e) > 0 {
				valueElems = append(valueElems, opValue{value: e, op: elem.op})
			}
		}
		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
}

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

Changes to strfun/slugify_test.go.

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

21
22
23
24
25
26
27







-







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"},
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
43
44
45
46
47
48
49

50
51
52
53
54
55
56







-







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

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

21
22
23
24
25
26
27







-







import (
	"testing"

	"zettelstore.de/z/strfun"
)

func TestTrimSpaceRight(t *testing.T) {
	t.Parallel()
	const space = "\t\v\r\f\n\u0085\u00a0\u2000\u3000"
	testcases := []struct {
		in  string
		exp string
	}{
		{"", ""},
		{"abc", "abc"},
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
46
47
48
49
50
51
52

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75







-
















-







		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, ""},
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
83
84
85
86
87
88
89

90
91
92
93
94
95
96







-







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

Changes to template/mustache_test.go.

225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
225
226
227
228
229
230
231

232
233
234
235
236
237
238







-







	if errMissing {
		tmpl.SetErrorOnMissing()
	}
	return render(tmpl, value)
}

func TestBasic(t *testing.T) {
	t.Parallel()
	for _, test := range tests {
		output, err := renderString(test.tmpl, false, test.context)
		if err != nil {
			t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err)
		} else if output != test.expected {
			t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output)
		}
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
257
258
259
260
261
262
263

264
265
266
267
268
269
270







-







	//dotted names(dot notation)
	{`"{{a.b.c}}" == ""`, map[string]interface{}{}, `"" == ""`, nil},
	{`"{{a.b.c.name}}" == ""`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "c": map[string]string{"name": "Jim"}}, `"" == ""`, nil},
	{`{{#a}}{{b.c}}{{/a}}`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "b": map[string]string{"c": "ERROR"}}, "", nil},
}

func TestMissing(t *testing.T) {
	t.Parallel()
	for _, test := range missing {
		output, err := renderString(test.tmpl, false, test.context)
		if err != nil {
			t.Error(err)
		} else if output != test.expected {
			t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output)
		}
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
287
288
289
290
291
292
293

294
295
296
297
298
299
300







-







	{`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")},
	{`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")},
	//invalid syntax - https://github.com/hoisie/mustache/issues/10
	{`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")},
}

func TestMalformed(t *testing.T) {
	t.Parallel()
	for _, test := range malformed {
		output, err := renderString(test.tmpl, false, test.context)
		if err != nil {
			if test.err == nil {
				t.Error(err)
			} else if test.err.Error() != err.Error() {
				t.Errorf("%q expected error %q but got error %q", test.tmpl, test.err.Error(), err.Error())
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
319
320
321
322
323
324
325

326
327
328
329
330
331
332







-







}

func (p Person) Name2() string {
	return p.FirstName + " " + p.LastName
}

func TestPointerReceiver(t *testing.T) {
	t.Parallel()
	p := Person{"John", "Smith"}
	tests := []struct {
		tmpl     string
		context  interface{}
		expected string
	}{
		{
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
409
410
411
412
413
414
415

416
417
418
419
420
421
422







-







				},
			},
		},
	},
}

func TestTags(t *testing.T) {
	t.Parallel()
	for _, test := range tagTests {
		testTags(t, &test)
	}
}

func testTags(t *testing.T, test *tagsTest) {
	tmpl, err := parseString(test.tmpl)

Changes to template/spec_test.go.

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
178
179
180
181
182
183
184

185
186
187
188
189
190
191







-







	if err != nil {
		curDir = os.Getenv("PWD")
	}
	return filepath.Join(curDir, "..", "testdata", "mustache")
}

func TestSpec(t *testing.T) {
	t.Parallel()
	root := getRoot()
	if _, err := os.Stat(root); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			t.Fatalf("Could not find the mustache testdata folder at %s'", root)
		}
		t.Fatal(err)
	}

Changes to testdata/markdown/README.md.

1

2

1
2
-
+

File `spec.json` is the CommonMark specification 0.30 test file.
File `spec.son` is the CommonMark specification 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
5195
5196
5197
5198
5199



5200
5201
5202
5203
5204
5205
5206
5207



5208
5209
5210
5211
5212
5213
5214
5215



5216
5217
5218
1
2
3
4
5


6
7
8
9
10
11
12
13


14
15
16
17
18
19
20
21


22
23
24
25
26
27
28
29


30
31
32
33
34
35
36
37


38
39
40
41
42
43
44
45


46
47
48
49
50
51
52
53


54
55
56
57
58
59
60
61


62
63
64
65
66
67
68
69


70
71
72
73
74
75
76
77


78
79
80
81
82
83
84
85


86
87
88
89
90
















































































































































































































































91
92



93
94
95
96
97
98
99
100



101
102
103
104
105
106
107
108



109
110
111
112
113
114
115
116



117
118
119
120
121
122
123
124



125
126
127
128
129
130
131
132



133
134
135
136
137
138
139
140



141
142
143
144
145
146
147
148



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





-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+



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


-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





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





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





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





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+


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




-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






-
-
+
+






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





-
-
-
+
+
+





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





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





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





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



-
-
-
-
-
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



-
-
-
-
-
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



-
-
-
-
-
+
+
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+





-
-
-
+
+
+



[
  {
    "markdown": "\tfoo\tbaz\t\tbim\n",
    "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n",
    "example": 1,
    "start_line": 356,
    "end_line": 361,
    "start_line": 352,
    "end_line": 357,
    "section": "Tabs"
  },
  {
    "markdown": "  \tfoo\tbaz\t\tbim\n",
    "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n",
    "example": 2,
    "start_line": 363,
    "end_line": 368,
    "start_line": 359,
    "end_line": 364,
    "section": "Tabs"
  },
  {
    "markdown": "    a\ta\n    ὐ\ta\n",
    "html": "<pre><code>a\ta\nὐ\ta\n</code></pre>\n",
    "example": 3,
    "start_line": 370,
    "end_line": 377,
    "start_line": 366,
    "end_line": 373,
    "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": 383,
    "end_line": 394,
    "start_line": 379,
    "end_line": 390,
    "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": 396,
    "end_line": 408,
    "start_line": 392,
    "end_line": 404,
    "section": "Tabs"
  },
  {
    "markdown": ">\t\tfoo\n",
    "html": "<blockquote>\n<pre><code>  foo\n</code></pre>\n</blockquote>\n",
    "example": 6,
    "start_line": 419,
    "end_line": 426,
    "start_line": 415,
    "end_line": 422,
    "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": 428,
    "end_line": 437,
    "start_line": 424,
    "end_line": 433,
    "section": "Tabs"
  },
  {
    "markdown": "    foo\n\tbar\n",
    "html": "<pre><code>foo\nbar\n</code></pre>\n",
    "example": 8,
    "start_line": 440,
    "end_line": 447,
    "start_line": 436,
    "end_line": 443,
    "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": 449,
    "end_line": 465,
    "start_line": 445,
    "end_line": 461,
    "section": "Tabs"
  },
  {
    "markdown": "#\tFoo\n",
    "html": "<h1>Foo</h1>\n",
    "example": 10,
    "start_line": 467,
    "end_line": 471,
    "start_line": 463,
    "end_line": 467,
    "section": "Tabs"
  },
  {
    "markdown": "*\t*\t*\t\n",
    "html": "<hr />\n",
    "example": 11,
    "start_line": 473,
    "end_line": 477,
    "start_line": 469,
    "end_line": 473,
    "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": 42,
    "start_line": 841,
    "end_line": 849,
    "example": 12,
    "start_line": 496,
    "end_line": 504,
    "section": "Precedence"
  },
  {
    "markdown": "***\n---\n___\n",
    "html": "<hr />\n<hr />\n<hr />\n",
    "example": 43,
    "start_line": 880,
    "end_line": 888,
    "example": 13,
    "start_line": 535,
    "end_line": 543,
    "section": "Thematic breaks"
  },
  {
    "markdown": "+++\n",
    "html": "<p>+++</p>\n",
    "example": 44,
    "start_line": 893,
    "end_line": 897,
    "example": 14,
    "start_line": 548,
    "end_line": 552,
    "section": "Thematic breaks"
  },
  {
    "markdown": "===\n",
    "html": "<p>===</p>\n",
    "example": 45,
    "start_line": 900,
    "end_line": 904,
    "example": 15,
    "start_line": 555,
    "end_line": 559,
    "section": "Thematic breaks"
  },
  {
    "markdown": "--\n**\n__\n",
    "html": "<p>--\n**\n__</p>\n",
    "example": 46,
    "start_line": 909,
    "end_line": 917,
    "example": 16,
    "start_line": 564,
    "end_line": 572,
    "section": "Thematic breaks"
  },
  {
    "markdown": " ***\n  ***\n   ***\n",
    "html": "<hr />\n<hr />\n<hr />\n",
    "example": 47,
    "start_line": 922,
    "end_line": 930,
    "example": 17,
    "start_line": 577,
    "end_line": 585,
    "section": "Thematic breaks"
  },
  {
    "markdown": "    ***\n",
    "html": "<pre><code>***\n</code></pre>\n",
    "example": 48,
    "start_line": 935,
    "end_line": 940,
    "example": 18,
    "start_line": 590,
    "end_line": 595,
    "section": "Thematic breaks"
  },
  {
    "markdown": "Foo\n    ***\n",
    "html": "<p>Foo\n***</p>\n",
    "example": 49,
    "start_line": 943,
    "end_line": 949,
    "example": 19,
    "start_line": 598,
    "end_line": 604,
    "section": "Thematic breaks"
  },
  {
    "markdown": "_____________________________________\n",
    "html": "<hr />\n",
    "example": 50,
    "start_line": 954,
    "end_line": 958,
    "example": 20,
    "start_line": 609,
    "end_line": 613,
    "section": "Thematic breaks"
  },
  {
    "markdown": " - - -\n",
    "html": "<hr />\n",
    "example": 51,
    "start_line": 963,
    "end_line": 967,
    "example": 21,
    "start_line": 618,
    "end_line": 622,
    "section": "Thematic breaks"
  },
  {
    "markdown": " **  * ** * ** * **\n",
    "html": "<hr />\n",
    "example": 52,
    "start_line": 970,
    "end_line": 974,
    "example": 22,
    "start_line": 625,
    "end_line": 629,
    "section": "Thematic breaks"
  },
  {
    "markdown": "-     -      -      -\n",
    "html": "<hr />\n",
    "example": 53,
    "start_line": 977,
    "end_line": 981,
    "example": 23,
    "start_line": 632,
    "end_line": 636,
    "section": "Thematic breaks"
  },
  {
    "markdown": "- - - -    \n",
    "html": "<hr />\n",
    "example": 54,
    "start_line": 986,
    "end_line": 990,
    "example": 24,
    "start_line": 641,
    "end_line": 645,
    "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": 55,
    "start_line": 995,
    "end_line": 1005,
    "example": 25,
    "start_line": 650,
    "end_line": 660,
    "section": "Thematic breaks"
  },
  {
    "markdown": " *-*\n",
    "html": "<p><em>-</em></p>\n",
    "example": 56,
    "start_line": 1011,
    "end_line": 1015,
    "example": 26,
    "start_line": 666,
    "end_line": 670,
    "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": 57,
    "start_line": 1020,
    "end_line": 1032,
    "example": 27,
    "start_line": 675,
    "end_line": 687,
    "section": "Thematic breaks"
  },
  {
    "markdown": "Foo\n***\nbar\n",
    "html": "<p>Foo</p>\n<hr />\n<p>bar</p>\n",
    "example": 58,
    "start_line": 1037,
    "end_line": 1045,
    "example": 28,
    "start_line": 692,
    "end_line": 700,
    "section": "Thematic breaks"
  },
  {
    "markdown": "Foo\n---\nbar\n",
    "html": "<h2>Foo</h2>\n<p>bar</p>\n",
    "example": 59,
    "start_line": 1054,
    "end_line": 1061,
    "example": 29,
    "start_line": 709,
    "end_line": 716,
    "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": 60,
    "start_line": 1067,
    "end_line": 1079,
    "example": 30,
    "start_line": 722,
    "end_line": 734,
    "section": "Thematic breaks"
  },
  {
    "markdown": "- Foo\n- * * *\n",
    "html": "<ul>\n<li>Foo</li>\n<li>\n<hr />\n</li>\n</ul>\n",
    "example": 61,
    "start_line": 1084,
    "end_line": 1094,
    "example": 31,
    "start_line": 739,
    "end_line": 749,
    "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": 62,
    "start_line": 1113,
    "end_line": 1127,
    "example": 32,
    "start_line": 768,
    "end_line": 782,
    "section": "ATX headings"
  },
  {
    "markdown": "####### foo\n",
    "html": "<p>####### foo</p>\n",
    "example": 63,
    "start_line": 1132,
    "end_line": 1136,
    "example": 33,
    "start_line": 787,
    "end_line": 791,
    "section": "ATX headings"
  },
  {
    "markdown": "#5 bolt\n\n#hashtag\n",
    "html": "<p>#5 bolt</p>\n<p>#hashtag</p>\n",
    "example": 64,
    "start_line": 1147,
    "end_line": 1154,
    "example": 34,
    "start_line": 802,
    "end_line": 809,
    "section": "ATX headings"
  },
  {
    "markdown": "\\## foo\n",
    "html": "<p>## foo</p>\n",
    "example": 65,
    "start_line": 1159,
    "end_line": 1163,
    "example": 35,
    "start_line": 814,
    "end_line": 818,
    "section": "ATX headings"
  },
  {
    "markdown": "# foo *bar* \\*baz\\*\n",
    "html": "<h1>foo <em>bar</em> *baz*</h1>\n",
    "example": 66,
    "start_line": 1168,
    "end_line": 1172,
    "example": 36,
    "start_line": 823,
    "end_line": 827,
    "section": "ATX headings"
  },
  {
    "markdown": "#                  foo                     \n",
    "html": "<h1>foo</h1>\n",
    "example": 67,
    "start_line": 1177,
    "end_line": 1181,
    "example": 37,
    "start_line": 832,
    "end_line": 836,
    "section": "ATX headings"
  },
  {
    "markdown": " ### foo\n  ## foo\n   # foo\n",
    "html": "<h3>foo</h3>\n<h2>foo</h2>\n<h1>foo</h1>\n",
    "example": 68,
    "start_line": 1186,
    "end_line": 1194,
    "example": 38,
    "start_line": 841,
    "end_line": 849,
    "section": "ATX headings"
  },
  {
    "markdown": "    # foo\n",
    "html": "<pre><code># foo\n</code></pre>\n",
    "example": 69,
    "start_line": 1199,
    "end_line": 1204,
    "example": 39,
    "start_line": 854,
    "end_line": 859,
    "section": "ATX headings"
  },
  {
    "markdown": "foo\n    # bar\n",
    "html": "<p>foo\n# bar</p>\n",
    "example": 70,
    "start_line": 1207,
    "end_line": 1213,
    "example": 40,
    "start_line": 862,
    "end_line": 868,
    "section": "ATX headings"
  },
  {
    "markdown": "## foo ##\n  ###   bar    ###\n",
    "html": "<h2>foo</h2>\n<h3>bar</h3>\n",
    "example": 71,
    "start_line": 1218,
    "end_line": 1224,
    "example": 41,
    "start_line": 873,
    "end_line": 879,
    "section": "ATX headings"
  },
  {
    "markdown": "# foo ##################################\n##### foo ##\n",
    "html": "<h1>foo</h1>\n<h5>foo</h5>\n",
    "example": 72,
    "start_line": 1229,
    "end_line": 1235,
    "example": 42,
    "start_line": 884,
    "end_line": 890,
    "section": "ATX headings"
  },
  {
    "markdown": "### foo ###     \n",
    "html": "<h3>foo</h3>\n",
    "example": 73,
    "start_line": 1240,
    "end_line": 1244,
    "example": 43,
    "start_line": 895,
    "end_line": 899,
    "section": "ATX headings"
  },
  {
    "markdown": "### foo ### b\n",
    "html": "<h3>foo ### b</h3>\n",
    "example": 74,
    "start_line": 1251,
    "end_line": 1255,
    "example": 44,
    "start_line": 906,
    "end_line": 910,
    "section": "ATX headings"
  },
  {
    "markdown": "# foo#\n",
    "html": "<h1>foo#</h1>\n",
    "example": 75,
    "start_line": 1260,
    "end_line": 1264,
    "example": 45,
    "start_line": 915,
    "end_line": 919,
    "section": "ATX headings"
  },
  {
    "markdown": "### foo \\###\n## foo #\\##\n# foo \\#\n",
    "html": "<h3>foo ###</h3>\n<h2>foo ###</h2>\n<h1>foo #</h1>\n",
    "example": 76,
    "start_line": 1270,
    "end_line": 1278,
    "example": 46,
    "start_line": 925,
    "end_line": 933,
    "section": "ATX headings"
  },
  {
    "markdown": "****\n## foo\n****\n",
    "html": "<hr />\n<h2>foo</h2>\n<hr />\n",
    "example": 77,
    "start_line": 1284,
    "end_line": 1292,
    "example": 47,
    "start_line": 939,
    "end_line": 947,
    "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": 78,
    "start_line": 1295,
    "end_line": 1303,
    "example": 48,
    "start_line": 950,
    "end_line": 958,
    "section": "ATX headings"
  },
  {
    "markdown": "## \n#\n### ###\n",
    "html": "<h2></h2>\n<h1></h1>\n<h3></h3>\n",
    "example": 79,
    "start_line": 1308,
    "end_line": 1316,
    "example": 49,
    "start_line": 963,
    "end_line": 971,
    "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": 80,
    "start_line": 1351,
    "end_line": 1360,
    "example": 50,
    "start_line": 1006,
    "end_line": 1015,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo *bar\nbaz*\n====\n",
    "html": "<h1>Foo <em>bar\nbaz</em></h1>\n",
    "example": 81,
    "start_line": 1365,
    "end_line": 1372,
    "example": 51,
    "start_line": 1020,
    "end_line": 1027,
    "section": "Setext headings"
  },
  {
    "markdown": "  Foo *bar\nbaz*\t\n====\n",
    "html": "<h1>Foo <em>bar\nbaz</em></h1>\n",
    "example": 82,
    "start_line": 1379,
    "end_line": 1386,
    "example": 52,
    "start_line": 1034,
    "end_line": 1041,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n-------------------------\n\nFoo\n=\n",
    "html": "<h2>Foo</h2>\n<h1>Foo</h1>\n",
    "example": 83,
    "start_line": 1391,
    "end_line": 1400,
    "example": 53,
    "start_line": 1046,
    "end_line": 1055,
    "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": 84,
    "start_line": 1406,
    "end_line": 1419,
    "example": 54,
    "start_line": 1061,
    "end_line": 1074,
    "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": 85,
    "start_line": 1424,
    "end_line": 1437,
    "example": 55,
    "start_line": 1079,
    "end_line": 1092,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n   ----      \n",
    "html": "<h2>Foo</h2>\n",
    "example": 86,
    "start_line": 1443,
    "end_line": 1448,
    "example": 56,
    "start_line": 1098,
    "end_line": 1103,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n    ---\n",
    "html": "<p>Foo\n---</p>\n",
    "example": 87,
    "start_line": 1453,
    "end_line": 1459,
    "example": 57,
    "start_line": 1108,
    "end_line": 1114,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n= =\n\nFoo\n--- -\n",
    "html": "<p>Foo\n= =</p>\n<p>Foo</p>\n<hr />\n",
    "example": 88,
    "start_line": 1464,
    "end_line": 1475,
    "example": 58,
    "start_line": 1119,
    "end_line": 1130,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo  \n-----\n",
    "html": "<h2>Foo</h2>\n",
    "example": 89,
    "start_line": 1480,
    "end_line": 1485,
    "example": 59,
    "start_line": 1135,
    "end_line": 1140,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\\\n----\n",
    "html": "<h2>Foo\\</h2>\n",
    "example": 90,
    "start_line": 1490,
    "end_line": 1495,
    "example": 60,
    "start_line": 1145,
    "end_line": 1150,
    "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": 91,
    "start_line": 1501,
    "end_line": 1514,
    "example": 61,
    "start_line": 1156,
    "end_line": 1169,
    "section": "Setext headings"
  },
  {
    "markdown": "> Foo\n---\n",
    "html": "<blockquote>\n<p>Foo</p>\n</blockquote>\n<hr />\n",
    "example": 92,
    "start_line": 1520,
    "end_line": 1528,
    "example": 62,
    "start_line": 1175,
    "end_line": 1183,
    "section": "Setext headings"
  },
  {
    "markdown": "> foo\nbar\n===\n",
    "html": "<blockquote>\n<p>foo\nbar\n===</p>\n</blockquote>\n",
    "example": 93,
    "start_line": 1531,
    "end_line": 1541,
    "example": 63,
    "start_line": 1186,
    "end_line": 1196,
    "section": "Setext headings"
  },
  {
    "markdown": "- Foo\n---\n",
    "html": "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n",
    "example": 94,
    "start_line": 1544,
    "end_line": 1552,
    "example": 64,
    "start_line": 1199,
    "end_line": 1207,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nBar\n---\n",
    "html": "<h2>Foo\nBar</h2>\n",
    "example": 95,
    "start_line": 1559,
    "end_line": 1566,
    "example": 65,
    "start_line": 1214,
    "end_line": 1221,
    "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": 96,
    "start_line": 1572,
    "end_line": 1584,
    "example": 66,
    "start_line": 1227,
    "end_line": 1239,
    "section": "Setext headings"
  },
  {
    "markdown": "\n====\n",
    "html": "<p>====</p>\n",
    "example": 97,
    "start_line": 1589,
    "end_line": 1594,
    "example": 67,
    "start_line": 1244,
    "end_line": 1249,
    "section": "Setext headings"
  },
  {
    "markdown": "---\n---\n",
    "html": "<hr />\n<hr />\n",
    "example": 98,
    "start_line": 1601,
    "end_line": 1607,
    "example": 68,
    "start_line": 1256,
    "end_line": 1262,
    "section": "Setext headings"
  },
  {
    "markdown": "- foo\n-----\n",
    "html": "<ul>\n<li>foo</li>\n</ul>\n<hr />\n",
    "example": 99,
    "start_line": 1610,
    "end_line": 1618,
    "example": 69,
    "start_line": 1265,
    "end_line": 1273,
    "section": "Setext headings"
  },
  {
    "markdown": "    foo\n---\n",
    "html": "<pre><code>foo\n</code></pre>\n<hr />\n",
    "example": 100,
    "start_line": 1621,
    "end_line": 1628,
    "example": 70,
    "start_line": 1276,
    "end_line": 1283,
    "section": "Setext headings"
  },
  {
    "markdown": "> foo\n-----\n",
    "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n",
    "example": 101,
    "start_line": 1631,
    "end_line": 1639,
    "example": 71,
    "start_line": 1286,
    "end_line": 1294,
    "section": "Setext headings"
  },
  {
    "markdown": "\\> foo\n------\n",
    "html": "<h2>&gt; foo</h2>\n",
    "example": 102,
    "start_line": 1645,
    "end_line": 1650,
    "example": 72,
    "start_line": 1300,
    "end_line": 1305,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\n\nbar\n---\nbaz\n",
    "html": "<p>Foo</p>\n<h2>bar</h2>\n<p>baz</p>\n",
    "example": 103,
    "start_line": 1676,
    "end_line": 1686,
    "example": 73,
    "start_line": 1331,
    "end_line": 1341,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nbar\n\n---\n\nbaz\n",
    "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n",
    "example": 104,
    "start_line": 1692,
    "end_line": 1704,
    "example": 74,
    "start_line": 1347,
    "end_line": 1359,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nbar\n* * *\nbaz\n",
    "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n",
    "example": 105,
    "start_line": 1710,
    "end_line": 1720,
    "example": 75,
    "start_line": 1365,
    "end_line": 1375,
    "section": "Setext headings"
  },
  {
    "markdown": "Foo\nbar\n\\---\nbaz\n",
    "html": "<p>Foo\nbar\n---\nbaz</p>\n",
    "example": 106,
    "start_line": 1725,
    "end_line": 1735,
    "example": 76,
    "start_line": 1380,
    "end_line": 1390,
    "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": 107,
    "start_line": 1753,
    "end_line": 1760,
    "example": 77,
    "start_line": 1408,
    "end_line": 1415,
    "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": 108,
    "start_line": 1767,
    "end_line": 1778,
    "example": 78,
    "start_line": 1422,
    "end_line": 1433,
    "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": 109,
    "start_line": 1781,
    "end_line": 1794,
    "example": 79,
    "start_line": 1436,
    "end_line": 1449,
    "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": 110,
    "start_line": 1801,
    "end_line": 1812,
    "example": 80,
    "start_line": 1456,
    "end_line": 1467,
    "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": 111,
    "start_line": 1817,
    "end_line": 1834,
    "example": 81,
    "start_line": 1472,
    "end_line": 1489,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    chunk1\n      \n      chunk2\n",
    "html": "<pre><code>chunk1\n  \n  chunk2\n</code></pre>\n",
    "example": 112,
    "start_line": 1840,
    "end_line": 1849,
    "example": 82,
    "start_line": 1495,
    "end_line": 1504,
    "section": "Indented code blocks"
  },
  {
    "markdown": "Foo\n    bar\n\n",
    "html": "<p>Foo\nbar</p>\n",
    "example": 113,
    "start_line": 1855,
    "end_line": 1862,
    "example": 83,
    "start_line": 1510,
    "end_line": 1517,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    foo\nbar\n",
    "html": "<pre><code>foo\n</code></pre>\n<p>bar</p>\n",
    "example": 114,
    "start_line": 1869,
    "end_line": 1876,
    "example": 84,
    "start_line": 1524,
    "end_line": 1531,
    "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": 115,
    "start_line": 1882,
    "end_line": 1897,
    "example": 85,
    "start_line": 1537,
    "end_line": 1552,
    "section": "Indented code blocks"
  },
  {
    "markdown": "        foo\n    bar\n",
    "html": "<pre><code>    foo\nbar\n</code></pre>\n",
    "example": 116,
    "start_line": 1902,
    "end_line": 1909,
    "example": 86,
    "start_line": 1557,
    "end_line": 1564,
    "section": "Indented code blocks"
  },
  {
    "markdown": "\n    \n    foo\n    \n\n",
    "html": "<pre><code>foo\n</code></pre>\n",
    "example": 117,
    "start_line": 1915,
    "end_line": 1924,
    "example": 87,
    "start_line": 1570,
    "end_line": 1579,
    "section": "Indented code blocks"
  },
  {
    "markdown": "    foo  \n",
    "html": "<pre><code>foo  \n</code></pre>\n",
    "example": 118,
    "start_line": 1929,
    "end_line": 1934,
    "example": 88,
    "start_line": 1584,
    "end_line": 1589,
    "section": "Indented code blocks"
  },
  {
    "markdown": "```\n<\n >\n```\n",
    "html": "<pre><code>&lt;\n &gt;\n</code></pre>\n",
    "example": 119,
    "start_line": 1984,
    "end_line": 1993,
    "example": 89,
    "start_line": 1639,
    "end_line": 1648,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~\n<\n >\n~~~\n",
    "html": "<pre><code>&lt;\n &gt;\n</code></pre>\n",
    "example": 120,
    "start_line": 1998,
    "end_line": 2007,
    "example": 90,
    "start_line": 1653,
    "end_line": 1662,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "``\nfoo\n``\n",
    "html": "<p><code>foo</code></p>\n",
    "example": 121,
    "start_line": 2011,
    "end_line": 2017,
    "example": 91,
    "start_line": 1666,
    "end_line": 1672,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\naaa\n~~~\n```\n",
    "html": "<pre><code>aaa\n~~~\n</code></pre>\n",
    "example": 122,
    "start_line": 2022,
    "end_line": 2031,
    "example": 92,
    "start_line": 1677,
    "end_line": 1686,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~\naaa\n```\n~~~\n",
    "html": "<pre><code>aaa\n```\n</code></pre>\n",
    "example": 123,
    "start_line": 2034,
    "end_line": 2043,
    "example": 93,
    "start_line": 1689,
    "end_line": 1698,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "````\naaa\n```\n``````\n",
    "html": "<pre><code>aaa\n```\n</code></pre>\n",
    "example": 124,
    "start_line": 2048,
    "end_line": 2057,
    "example": 94,
    "start_line": 1703,
    "end_line": 1712,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~~\naaa\n~~~\n~~~~\n",
    "html": "<pre><code>aaa\n~~~\n</code></pre>\n",
    "example": 125,
    "start_line": 2060,
    "end_line": 2069,
    "example": 95,
    "start_line": 1715,
    "end_line": 1724,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n",
    "html": "<pre><code></code></pre>\n",
    "example": 126,
    "start_line": 2075,
    "end_line": 2079,
    "example": 96,
    "start_line": 1730,
    "end_line": 1734,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "`````\n\n```\naaa\n",
    "html": "<pre><code>\n```\naaa\n</code></pre>\n",
    "example": 127,
    "start_line": 2082,
    "end_line": 2092,
    "example": 97,
    "start_line": 1737,
    "end_line": 1747,
    "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": 128,
    "start_line": 2095,
    "end_line": 2106,
    "example": 98,
    "start_line": 1750,
    "end_line": 1761,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n\n  \n```\n",
    "html": "<pre><code>\n  \n</code></pre>\n",
    "example": 129,
    "start_line": 2111,
    "end_line": 2120,
    "example": 99,
    "start_line": 1766,
    "end_line": 1775,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n```\n",
    "html": "<pre><code></code></pre>\n",
    "example": 130,
    "start_line": 2125,
    "end_line": 2130,
    "example": 100,
    "start_line": 1780,
    "end_line": 1785,
    "section": "Fenced code blocks"
  },
  {
    "markdown": " ```\n aaa\naaa\n```\n",
    "html": "<pre><code>aaa\naaa\n</code></pre>\n",
    "example": 131,
    "start_line": 2137,
    "end_line": 2146,
    "example": 101,
    "start_line": 1792,
    "end_line": 1801,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "  ```\naaa\n  aaa\naaa\n  ```\n",
    "html": "<pre><code>aaa\naaa\naaa\n</code></pre>\n",
    "example": 132,
    "start_line": 2149,
    "end_line": 2160,
    "example": 102,
    "start_line": 1804,
    "end_line": 1815,
    "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": 133,
    "start_line": 2163,
    "end_line": 2174,
    "example": 103,
    "start_line": 1818,
    "end_line": 1829,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "    ```\n    aaa\n    ```\n",
    "html": "<pre><code>```\naaa\n```\n</code></pre>\n",
    "example": 134,
    "start_line": 2179,
    "end_line": 2188,
    "example": 104,
    "start_line": 1834,
    "end_line": 1843,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\naaa\n  ```\n",
    "html": "<pre><code>aaa\n</code></pre>\n",
    "example": 135,
    "start_line": 2194,
    "end_line": 2201,
    "example": 105,
    "start_line": 1849,
    "end_line": 1856,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "   ```\naaa\n  ```\n",
    "html": "<pre><code>aaa\n</code></pre>\n",
    "example": 136,
    "start_line": 2204,
    "end_line": 2211,
    "example": 106,
    "start_line": 1859,
    "end_line": 1866,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\naaa\n    ```\n",
    "html": "<pre><code>aaa\n    ```\n</code></pre>\n",
    "example": 137,
    "start_line": 2216,
    "end_line": 2224,
    "example": 107,
    "start_line": 1871,
    "end_line": 1879,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "``` ```\naaa\n",
    "html": "<p><code> </code>\naaa</p>\n",
    "example": 138,
    "start_line": 2230,
    "end_line": 2236,
    "example": 108,
    "start_line": 1885,
    "end_line": 1891,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~~~~\naaa\n~~~ ~~\n",
    "html": "<pre><code>aaa\n~~~ ~~\n</code></pre>\n",
    "example": 139,
    "start_line": 2239,
    "end_line": 2247,
    "example": 109,
    "start_line": 1894,
    "end_line": 1902,
    "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": 140,
    "start_line": 2253,
    "end_line": 2264,
    "example": 110,
    "start_line": 1908,
    "end_line": 1919,
    "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": 141,
    "start_line": 2270,
    "end_line": 2282,
    "example": 111,
    "start_line": 1925,
    "end_line": 1937,
    "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": 142,
    "start_line": 2292,
    "end_line": 2303,
    "example": 112,
    "start_line": 1947,
    "end_line": 1958,
    "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": 143,
    "start_line": 2306,
    "end_line": 2317,
    "example": 113,
    "start_line": 1961,
    "end_line": 1972,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "````;\n````\n",
    "html": "<pre><code class=\"language-;\"></code></pre>\n",
    "example": 144,
    "start_line": 2320,
    "end_line": 2325,
    "example": 114,
    "start_line": 1975,
    "end_line": 1980,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "``` aa ```\nfoo\n",
    "html": "<p><code>aa</code>\nfoo</p>\n",
    "example": 145,
    "start_line": 2330,
    "end_line": 2336,
    "example": 115,
    "start_line": 1985,
    "end_line": 1991,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "~~~ aa ``` ~~~\nfoo\n~~~\n",
    "html": "<pre><code class=\"language-aa\">foo\n</code></pre>\n",
    "example": 146,
    "start_line": 2341,
    "end_line": 2348,
    "example": 116,
    "start_line": 1996,
    "end_line": 2003,
    "section": "Fenced code blocks"
  },
  {
    "markdown": "```\n``` aaa\n```\n",
    "html": "<pre><code>``` aaa\n</code></pre>\n",
    "example": 147,
    "start_line": 2353,
    "end_line": 2360,
    "example": 117,
    "start_line": 2008,
    "end_line": 2015,
    "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": 148,
    "start_line": 2432,
    "end_line": 2447,
    "example": 118,
    "start_line": 2087,
    "end_line": 2102,
    "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": 149,
    "start_line": 2461,
    "end_line": 2480,
    "example": 119,
    "start_line": 2116,
    "end_line": 2135,
    "section": "HTML blocks"
  },
  {
    "markdown": " <div>\n  *hello*\n         <foo><a>\n",
    "html": " <div>\n  *hello*\n         <foo><a>\n",
    "example": 150,
    "start_line": 2483,
    "end_line": 2491,
    "example": 120,
    "start_line": 2138,
    "end_line": 2146,
    "section": "HTML blocks"
  },
  {
    "markdown": "</div>\n*foo*\n",
    "html": "</div>\n*foo*\n",
    "example": 151,
    "start_line": 2496,
    "end_line": 2502,
    "example": 121,
    "start_line": 2151,
    "end_line": 2157,
    "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": 152,
    "start_line": 2507,
    "end_line": 2517,
    "example": 122,
    "start_line": 2162,
    "end_line": 2172,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div id=\"foo\"\n  class=\"bar\">\n</div>\n",
    "html": "<div id=\"foo\"\n  class=\"bar\">\n</div>\n",
    "example": 153,
    "start_line": 2523,
    "end_line": 2531,
    "example": 123,
    "start_line": 2178,
    "end_line": 2186,
    "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": 154,
    "start_line": 2534,
    "end_line": 2542,
    "example": 124,
    "start_line": 2189,
    "end_line": 2197,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div>\n*foo*\n\n*bar*\n",
    "html": "<div>\n*foo*\n<p><em>bar</em></p>\n",
    "example": 155,
    "start_line": 2546,
    "end_line": 2555,
    "example": 125,
    "start_line": 2201,
    "end_line": 2210,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div id=\"foo\"\n*hi*\n",
    "html": "<div id=\"foo\"\n*hi*\n",
    "example": 156,
    "start_line": 2562,
    "end_line": 2568,
    "example": 126,
    "start_line": 2217,
    "end_line": 2223,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div class\nfoo\n",
    "html": "<div class\nfoo\n",
    "example": 157,
    "start_line": 2571,
    "end_line": 2577,
    "example": 127,
    "start_line": 2226,
    "end_line": 2232,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div *???-&&&-<---\n*foo*\n",
    "html": "<div *???-&&&-<---\n*foo*\n",
    "example": 158,
    "start_line": 2583,
    "end_line": 2589,
    "example": 128,
    "start_line": 2238,
    "end_line": 2244,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div><a href=\"bar\">*foo*</a></div>\n",
    "html": "<div><a href=\"bar\">*foo*</a></div>\n",
    "example": 159,
    "start_line": 2595,
    "end_line": 2599,
    "example": 129,
    "start_line": 2250,
    "end_line": 2254,
    "section": "HTML blocks"
  },
  {
    "markdown": "<table><tr><td>\nfoo\n</td></tr></table>\n",
    "html": "<table><tr><td>\nfoo\n</td></tr></table>\n",
    "example": 160,
    "start_line": 2602,
    "end_line": 2610,
    "example": 130,
    "start_line": 2257,
    "end_line": 2265,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div></div>\n``` c\nint x = 33;\n```\n",
    "html": "<div></div>\n``` c\nint x = 33;\n```\n",
    "example": 161,
    "start_line": 2619,
    "end_line": 2629,
    "example": 131,
    "start_line": 2274,
    "end_line": 2284,
    "section": "HTML blocks"
  },
  {
    "markdown": "<a href=\"foo\">\n*bar*\n</a>\n",
    "html": "<a href=\"foo\">\n*bar*\n</a>\n",
    "example": 162,
    "start_line": 2636,
    "end_line": 2644,
    "example": 132,
    "start_line": 2291,
    "end_line": 2299,
    "section": "HTML blocks"
  },
  {
    "markdown": "<Warning>\n*bar*\n</Warning>\n",
    "html": "<Warning>\n*bar*\n</Warning>\n",
    "example": 163,
    "start_line": 2649,
    "end_line": 2657,
    "example": 133,
    "start_line": 2304,
    "end_line": 2312,
    "section": "HTML blocks"
  },
  {
    "markdown": "<i class=\"foo\">\n*bar*\n</i>\n",
    "html": "<i class=\"foo\">\n*bar*\n</i>\n",
    "example": 164,
    "start_line": 2660,
    "end_line": 2668,
    "example": 134,
    "start_line": 2315,
    "end_line": 2323,
    "section": "HTML blocks"
  },
  {
    "markdown": "</ins>\n*bar*\n",
    "html": "</ins>\n*bar*\n",
    "example": 165,
    "start_line": 2671,
    "end_line": 2677,
    "example": 135,
    "start_line": 2326,
    "end_line": 2332,
    "section": "HTML blocks"
  },
  {
    "markdown": "<del>\n*foo*\n</del>\n",
    "html": "<del>\n*foo*\n</del>\n",
    "example": 166,
    "start_line": 2686,
    "end_line": 2694,
    "example": 136,
    "start_line": 2341,
    "end_line": 2349,
    "section": "HTML blocks"
  },
  {
    "markdown": "<del>\n\n*foo*\n\n</del>\n",
    "html": "<del>\n<p><em>foo</em></p>\n</del>\n",
    "example": 167,
    "start_line": 2701,
    "end_line": 2711,
    "example": 137,
    "start_line": 2356,
    "end_line": 2366,
    "section": "HTML blocks"
  },
  {
    "markdown": "<del>*foo*</del>\n",
    "html": "<p><del><em>foo</em></del></p>\n",
    "example": 168,
    "start_line": 2719,
    "end_line": 2723,
    "example": 138,
    "start_line": 2374,
    "end_line": 2378,
    "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": 169,
    "start_line": 2735,
    "end_line": 2751,
    "example": 139,
    "start_line": 2390,
    "end_line": 2406,
    "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": 170,
    "start_line": 2756,
    "end_line": 2770,
    "example": 140,
    "start_line": 2411,
    "end_line": 2425,
    "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": 172,
    "start_line": 2795,
    "end_line": 2811,
    "example": 141,
    "start_line": 2430,
    "end_line": 2446,
    "section": "HTML blocks"
  },
  {
    "markdown": "<style\n  type=\"text/css\">\n\nfoo\n",
    "html": "<style\n  type=\"text/css\">\n\nfoo\n",
    "example": 173,
    "start_line": 2818,
    "end_line": 2828,
    "example": 142,
    "start_line": 2453,
    "end_line": 2463,
    "section": "HTML blocks"
  },
  {
    "markdown": "> <div>\n> foo\n\nbar\n",
    "html": "<blockquote>\n<div>\nfoo\n</blockquote>\n<p>bar</p>\n",
    "example": 174,
    "start_line": 2831,
    "end_line": 2842,
    "example": 143,
    "start_line": 2466,
    "end_line": 2477,
    "section": "HTML blocks"
  },
  {
    "markdown": "- <div>\n- foo\n",
    "html": "<ul>\n<li>\n<div>\n</li>\n<li>foo</li>\n</ul>\n",
    "example": 175,
    "start_line": 2845,
    "end_line": 2855,
    "example": 144,
    "start_line": 2480,
    "end_line": 2490,
    "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": 176,
    "start_line": 2860,
    "end_line": 2866,
    "example": 145,
    "start_line": 2495,
    "end_line": 2501,
    "section": "HTML blocks"
  },
  {
    "markdown": "<!-- foo -->*bar*\n*baz*\n",
    "html": "<!-- foo -->*bar*\n<p><em>baz</em></p>\n",
    "example": 177,
    "start_line": 2869,
    "end_line": 2875,
    "example": 146,
    "start_line": 2504,
    "end_line": 2510,
    "section": "HTML blocks"
  },
  {
    "markdown": "<script>\nfoo\n</script>1. *bar*\n",
    "html": "<script>\nfoo\n</script>1. *bar*\n",
    "example": 178,
    "start_line": 2881,
    "end_line": 2889,
    "example": 147,
    "start_line": 2516,
    "end_line": 2524,
    "section": "HTML blocks"
  },
  {
    "markdown": "<!-- Foo\n\nbar\n   baz -->\nokay\n",
    "html": "<!-- Foo\n\nbar\n   baz -->\n<p>okay</p>\n",
    "example": 179,
    "start_line": 2894,
    "end_line": 2906,
    "example": 148,
    "start_line": 2529,
    "end_line": 2541,
    "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": 180,
    "start_line": 2912,
    "end_line": 2926,
    "example": 149,
    "start_line": 2547,
    "end_line": 2561,
    "section": "HTML blocks"
  },
  {
    "markdown": "<!DOCTYPE html>\n",
    "html": "<!DOCTYPE html>\n",
    "example": 181,
    "start_line": 2931,
    "end_line": 2935,
    "example": 150,
    "start_line": 2566,
    "end_line": 2570,
    "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": 182,
    "start_line": 2940,
    "end_line": 2968,
    "example": 151,
    "start_line": 2575,
    "end_line": 2603,
    "section": "HTML blocks"
  },
  {
    "markdown": "  <!-- foo -->\n\n    <!-- foo -->\n",
    "html": "  <!-- foo -->\n<pre><code>&lt;!-- foo --&gt;\n</code></pre>\n",
    "example": 183,
    "start_line": 2974,
    "end_line": 2982,
    "example": 152,
    "start_line": 2608,
    "end_line": 2616,
    "section": "HTML blocks"
  },
  {
    "markdown": "  <div>\n\n    <div>\n",
    "html": "  <div>\n<pre><code>&lt;div&gt;\n</code></pre>\n",
    "example": 184,
    "start_line": 2985,
    "end_line": 2993,
    "example": 153,
    "start_line": 2619,
    "end_line": 2627,
    "section": "HTML blocks"
  },
  {
    "markdown": "Foo\n<div>\nbar\n</div>\n",
    "html": "<p>Foo</p>\n<div>\nbar\n</div>\n",
    "example": 185,
    "start_line": 2999,
    "end_line": 3009,
    "example": 154,
    "start_line": 2633,
    "end_line": 2643,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div>\nbar\n</div>\n*foo*\n",
    "html": "<div>\nbar\n</div>\n*foo*\n",
    "example": 186,
    "start_line": 3016,
    "end_line": 3026,
    "example": 155,
    "start_line": 2650,
    "end_line": 2660,
    "section": "HTML blocks"
  },
  {
    "markdown": "Foo\n<a href=\"bar\">\nbaz\n",
    "html": "<p>Foo\n<a href=\"bar\">\nbaz</p>\n",
    "example": 187,
    "start_line": 3031,
    "end_line": 3039,
    "example": 156,
    "start_line": 2665,
    "end_line": 2673,
    "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": 188,
    "start_line": 3072,
    "end_line": 3082,
    "example": 157,
    "start_line": 2706,
    "end_line": 2716,
    "section": "HTML blocks"
  },
  {
    "markdown": "<div>\n*Emphasized* text.\n</div>\n",
    "html": "<div>\n*Emphasized* text.\n</div>\n",
    "example": 189,
    "start_line": 3085,
    "end_line": 3093,
    "example": 158,
    "start_line": 2719,
    "end_line": 2727,
    "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": 190,
    "start_line": 3107,
    "end_line": 3127,
    "example": 159,
    "start_line": 2741,
    "end_line": 2761,
    "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": 191,
    "start_line": 3134,
    "end_line": 3155,
    "example": 160,
    "start_line": 2768,
    "end_line": 2789,
    "section": "HTML blocks"
  },
  {
    "markdown": "[foo]: /url \"title\"\n\n[foo]\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 192,
    "start_line": 3183,
    "end_line": 3189,
    "example": 161,
    "start_line": 2816,
    "end_line": 2822,
    "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": 193,
    "start_line": 3192,
    "end_line": 3200,
    "example": 162,
    "start_line": 2825,
    "end_line": 2833,
    "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": 194,
    "start_line": 3203,
    "end_line": 3209,
    "example": 163,
    "start_line": 2836,
    "end_line": 2842,
    "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": 195,
    "start_line": 3212,
    "end_line": 3220,
    "example": 164,
    "start_line": 2845,
    "end_line": 2853,
    "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": 196,
    "start_line": 3225,
    "end_line": 3239,
    "example": 165,
    "start_line": 2858,
    "end_line": 2872,
    "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": 197,
    "start_line": 3244,
    "end_line": 3254,
    "example": 166,
    "start_line": 2877,
    "end_line": 2887,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]:\n/url\n\n[foo]\n",
    "html": "<p><a href=\"/url\">foo</a></p>\n",
    "example": 198,
    "start_line": 3259,
    "end_line": 3266,
    "example": 167,
    "start_line": 2892,
    "end_line": 2899,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]:\n\n[foo]\n",
    "html": "<p>[foo]:</p>\n<p>[foo]</p>\n",
    "example": 199,
    "start_line": 3271,
    "end_line": 3278,
    "example": 168,
    "start_line": 2904,
    "end_line": 2911,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: <>\n\n[foo]\n",
    "html": "<p><a href=\"\">foo</a></p>\n",
    "example": 200,
    "start_line": 3283,
    "end_line": 3289,
    "example": 169,
    "start_line": 2916,
    "end_line": 2922,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: <bar>(baz)\n\n[foo]\n",
    "html": "<p>[foo]: <bar>(baz)</p>\n<p>[foo]</p>\n",
    "example": 201,
    "start_line": 3294,
    "end_line": 3301,
    "example": 170,
    "start_line": 2927,
    "end_line": 2934,
    "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": 202,
    "start_line": 3307,
    "end_line": 3313,
    "example": 171,
    "start_line": 2940,
    "end_line": 2946,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]\n\n[foo]: url\n",
    "html": "<p><a href=\"url\">foo</a></p>\n",
    "example": 203,
    "start_line": 3318,
    "end_line": 3324,
    "example": 172,
    "start_line": 2951,
    "end_line": 2957,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n",
    "html": "<p><a href=\"first\">foo</a></p>\n",
    "example": 204,
    "start_line": 3330,
    "end_line": 3337,
    "example": 173,
    "start_line": 2963,
    "end_line": 2970,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[FOO]: /url\n\n[Foo]\n",
    "html": "<p><a href=\"/url\">Foo</a></p>\n",
    "example": 205,
    "start_line": 3343,
    "end_line": 3349,
    "example": 174,
    "start_line": 2976,
    "end_line": 2982,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n",
    "html": "<p><a href=\"/%CF%86%CE%BF%CF%85\">αγω</a></p>\n",
    "example": 206,
    "start_line": 3352,
    "end_line": 3358,
    "example": 175,
    "start_line": 2985,
    "end_line": 2991,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\n",
    "html": "",
    "example": 207,
    "start_line": 3367,
    "end_line": 3370,
    "example": 176,
    "start_line": 2997,
    "end_line": 3000,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[\nfoo\n]: /url\nbar\n",
    "html": "<p>bar</p>\n",
    "example": 208,
    "start_line": 3375,
    "end_line": 3382,
    "example": 177,
    "start_line": 3005,
    "end_line": 3012,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url \"title\" ok\n",
    "html": "<p>[foo]: /url &quot;title&quot; ok</p>\n",
    "example": 209,
    "start_line": 3388,
    "end_line": 3392,
    "example": 178,
    "start_line": 3018,
    "end_line": 3022,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\n\"title\" ok\n",
    "html": "<p>&quot;title&quot; ok</p>\n",
    "example": 210,
    "start_line": 3397,
    "end_line": 3402,
    "example": 179,
    "start_line": 3027,
    "end_line": 3032,
    "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": 211,
    "start_line": 3408,
    "end_line": 3416,
    "example": 180,
    "start_line": 3038,
    "end_line": 3046,
    "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": 212,
    "start_line": 3422,
    "end_line": 3432,
    "example": 181,
    "start_line": 3052,
    "end_line": 3062,
    "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": 213,
    "start_line": 3437,
    "end_line": 3446,
    "example": 182,
    "start_line": 3067,
    "end_line": 3076,
    "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": 214,
    "start_line": 3452,
    "end_line": 3461,
    "example": 183,
    "start_line": 3082,
    "end_line": 3091,
    "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": 215,
    "start_line": 3463,
    "end_line": 3471,
    "example": 184,
    "start_line": 3093,
    "end_line": 3101,
    "section": "Link reference definitions"
  },
  {
    "markdown": "[foo]: /url\n===\n[foo]\n",
    "html": "<p>===\n<a href=\"/url\">foo</a></p>\n",
    "example": 216,
    "start_line": 3473,
    "end_line": 3480,
    "example": 185,
    "start_line": 3103,
    "end_line": 3110,
    "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": 217,
    "start_line": 3486,
    "end_line": 3499,
    "example": 186,
    "start_line": 3116,
    "end_line": 3129,
    "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": 218,
    "start_line": 3507,
    "end_line": 3515,
    "example": 187,
    "start_line": 3137,
    "end_line": 3145,
    "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": 219,
    "start_line": 3529,
    "end_line": 3536,
    "example": 189,
    "start_line": 3171,
    "end_line": 3178,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa\nbbb\n\nccc\nddd\n",
    "html": "<p>aaa\nbbb</p>\n<p>ccc\nddd</p>\n",
    "example": 220,
    "start_line": 3541,
    "end_line": 3552,
    "example": 190,
    "start_line": 3183,
    "end_line": 3194,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa\n\n\nbbb\n",
    "html": "<p>aaa</p>\n<p>bbb</p>\n",
    "example": 221,
    "start_line": 3557,
    "end_line": 3565,
    "example": 191,
    "start_line": 3199,
    "end_line": 3207,
    "section": "Paragraphs"
  },
  {
    "markdown": "  aaa\n bbb\n",
    "html": "<p>aaa\nbbb</p>\n",
    "example": 222,
    "start_line": 3570,
    "end_line": 3576,
    "example": 192,
    "start_line": 3212,
    "end_line": 3218,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa\n             bbb\n                                       ccc\n",
    "html": "<p>aaa\nbbb\nccc</p>\n",
    "example": 223,
    "start_line": 3582,
    "end_line": 3590,
    "example": 193,
    "start_line": 3224,
    "end_line": 3232,
    "section": "Paragraphs"
  },
  {
    "markdown": "   aaa\nbbb\n",
    "html": "<p>aaa\nbbb</p>\n",
    "example": 224,
    "start_line": 3596,
    "end_line": 3602,
    "example": 194,
    "start_line": 3238,
    "end_line": 3244,
    "section": "Paragraphs"
  },
  {
    "markdown": "    aaa\nbbb\n",
    "html": "<pre><code>aaa\n</code></pre>\n<p>bbb</p>\n",
    "example": 225,
    "start_line": 3605,
    "end_line": 3612,
    "example": 195,
    "start_line": 3247,
    "end_line": 3254,
    "section": "Paragraphs"
  },
  {
    "markdown": "aaa     \nbbb     \n",
    "html": "<p>aaa<br />\nbbb</p>\n",
    "example": 226,
    "start_line": 3619,
    "end_line": 3625,
    "example": 196,
    "start_line": 3261,
    "end_line": 3267,
    "section": "Paragraphs"
  },
  {
    "markdown": "  \n\naaa\n  \n\n# aaa\n\n  \n",
    "html": "<p>aaa</p>\n<h1>aaa</h1>\n",
    "example": 227,
    "start_line": 3636,
    "end_line": 3648,
    "example": 197,
    "start_line": 3278,
    "end_line": 3290,
    "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": 228,
    "start_line": 3704,
    "end_line": 3714,
    "example": 198,
    "start_line": 3344,
    "end_line": 3354,
    "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": 229,
    "start_line": 3719,
    "end_line": 3729,
    "example": 199,
    "start_line": 3359,
    "end_line": 3369,
    "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": 230,
    "start_line": 3734,
    "end_line": 3744,
    "example": 200,
    "start_line": 3374,
    "end_line": 3384,
    "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": 231,
    "start_line": 3749,
    "end_line": 3758,
    "example": 201,
    "start_line": 3389,
    "end_line": 3398,
    "section": "Block quotes"
  },
  {
    "markdown": "> # Foo\n> bar\nbaz\n",
    "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n",
    "example": 232,
    "start_line": 3764,
    "end_line": 3774,
    "example": 202,
    "start_line": 3404,
    "end_line": 3414,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\nbaz\n> foo\n",
    "html": "<blockquote>\n<p>bar\nbaz\nfoo</p>\n</blockquote>\n",
    "example": 233,
    "start_line": 3780,
    "end_line": 3790,
    "example": 203,
    "start_line": 3420,
    "end_line": 3430,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n---\n",
    "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n",
    "example": 234,
    "start_line": 3804,
    "end_line": 3812,
    "example": 204,
    "start_line": 3444,
    "end_line": 3452,
    "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": 235,
    "start_line": 3824,
    "end_line": 3836,
    "example": 205,
    "start_line": 3464,
    "end_line": 3476,
    "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": 236,
    "start_line": 3842,
    "end_line": 3852,
    "example": 206,
    "start_line": 3482,
    "end_line": 3492,
    "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": 237,
    "start_line": 3855,
    "end_line": 3865,
    "example": 207,
    "start_line": 3495,
    "end_line": 3505,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n    - bar\n",
    "html": "<blockquote>\n<p>foo\n- bar</p>\n</blockquote>\n",
    "example": 238,
    "start_line": 3871,
    "end_line": 3879,
    "example": 208,
    "start_line": 3511,
    "end_line": 3519,
    "section": "Block quotes"
  },
  {
    "markdown": ">\n",
    "html": "<blockquote>\n</blockquote>\n",
    "example": 239,
    "start_line": 3895,
    "end_line": 3900,
    "example": 209,
    "start_line": 3535,
    "end_line": 3540,
    "section": "Block quotes"
  },
  {
    "markdown": ">\n>  \n> \n",
    "html": "<blockquote>\n</blockquote>\n",
    "example": 240,
    "start_line": 3903,
    "end_line": 3910,
    "example": 210,
    "start_line": 3543,
    "end_line": 3550,
    "section": "Block quotes"
  },
  {
    "markdown": ">\n> foo\n>  \n",
    "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n",
    "example": 241,
    "start_line": 3915,
    "end_line": 3923,
    "example": 211,
    "start_line": 3555,
    "end_line": 3563,
    "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": 242,
    "start_line": 3928,
    "end_line": 3939,
    "example": 212,
    "start_line": 3568,
    "end_line": 3579,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n> bar\n",
    "html": "<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n",
    "example": 243,
    "start_line": 3950,
    "end_line": 3958,
    "example": 213,
    "start_line": 3590,
    "end_line": 3598,
    "section": "Block quotes"
  },
  {
    "markdown": "> foo\n>\n> bar\n",
    "html": "<blockquote>\n<p>foo</p>\n<p>bar</p>\n</blockquote>\n",
    "example": 244,
    "start_line": 3963,
    "end_line": 3972,
    "example": 214,
    "start_line": 3603,
    "end_line": 3612,
    "section": "Block quotes"
  },
  {
    "markdown": "foo\n> bar\n",
    "html": "<p>foo</p>\n<blockquote>\n<p>bar</p>\n</blockquote>\n",
    "example": 245,
    "start_line": 3977,
    "end_line": 3985,
    "example": 215,
    "start_line": 3617,
    "end_line": 3625,
    "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": 246,
    "start_line": 3991,
    "end_line": 4003,
    "example": 216,
    "start_line": 3631,
    "end_line": 3643,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\nbaz\n",
    "html": "<blockquote>\n<p>bar\nbaz</p>\n</blockquote>\n",
    "example": 247,
    "start_line": 4009,
    "end_line": 4017,
    "example": 217,
    "start_line": 3649,
    "end_line": 3657,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\n\nbaz\n",
    "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n",
    "example": 248,
    "start_line": 4020,
    "end_line": 4029,
    "example": 218,
    "start_line": 3660,
    "end_line": 3669,
    "section": "Block quotes"
  },
  {
    "markdown": "> bar\n>\nbaz\n",
    "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n",
    "example": 249,
    "start_line": 4032,
    "end_line": 4041,
    "example": 219,
    "start_line": 3672,
    "end_line": 3681,
    "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": 250,
    "start_line": 4048,
    "end_line": 4060,
    "example": 220,
    "start_line": 3688,
    "end_line": 3700,
    "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": 251,
    "start_line": 4063,
    "end_line": 4077,
    "example": 221,
    "start_line": 3703,
    "end_line": 3717,
    "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": 252,
    "start_line": 4085,
    "end_line": 4097,
    "example": 222,
    "start_line": 3725,
    "end_line": 3737,
    "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": 253,
    "start_line": 4139,
    "end_line": 4154,
    "example": 223,
    "start_line": 3779,
    "end_line": 3794,
    "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": 254,
    "start_line": 4161,
    "end_line": 4180,
    "example": 224,
    "start_line": 3801,
    "end_line": 3820,
    "section": "List items"
  },
  {
    "markdown": "- one\n\n two\n",
    "html": "<ul>\n<li>one</li>\n</ul>\n<p>two</p>\n",
    "example": 255,
    "start_line": 4194,
    "end_line": 4203,
    "example": 225,
    "start_line": 3834,
    "end_line": 3843,
    "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": 256,
    "start_line": 4206,
    "end_line": 4217,
    "example": 226,
    "start_line": 3846,
    "end_line": 3857,
    "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": 257,
    "start_line": 4220,
    "end_line": 4230,
    "example": 227,
    "start_line": 3860,
    "end_line": 3870,
    "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": 258,
    "start_line": 4233,
    "end_line": 4244,
    "example": 228,
    "start_line": 3873,
    "end_line": 3884,
    "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": 259,
    "start_line": 4255,
    "end_line": 4270,
    "example": 229,
    "start_line": 3895,
    "end_line": 3910,
    "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": 260,
    "start_line": 4282,
    "end_line": 4295,
    "example": 230,
    "start_line": 3922,
    "end_line": 3935,
    "section": "List items"
  },
  {
    "markdown": "-one\n\n2.two\n",
    "html": "<p>-one</p>\n<p>2.two</p>\n",
    "example": 261,
    "start_line": 4301,
    "end_line": 4308,
    "example": 231,
    "start_line": 3941,
    "end_line": 3948,
    "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": 262,
    "start_line": 4314,
    "end_line": 4326,
    "example": 232,
    "start_line": 3954,
    "end_line": 3966,
    "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": 263,
    "start_line": 4331,
    "end_line": 4353,
    "example": 233,
    "start_line": 3971,
    "end_line": 3993,
    "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": 264,
    "start_line": 4359,
    "end_line": 4377,
    "example": 234,
    "start_line": 3999,
    "end_line": 4017,
    "section": "List items"
  },
  {
    "markdown": "123456789. ok\n",
    "html": "<ol start=\"123456789\">\n<li>ok</li>\n</ol>\n",
    "example": 265,
    "start_line": 4381,
    "end_line": 4387,
    "example": 235,
    "start_line": 4021,
    "end_line": 4027,
    "section": "List items"
  },
  {
    "markdown": "1234567890. not ok\n",
    "html": "<p>1234567890. not ok</p>\n",
    "example": 266,
    "start_line": 4390,
    "end_line": 4394,
    "example": 236,
    "start_line": 4030,
    "end_line": 4034,
    "section": "List items"
  },
  {
    "markdown": "0. ok\n",
    "html": "<ol start=\"0\">\n<li>ok</li>\n</ol>\n",
    "example": 267,
    "start_line": 4399,
    "end_line": 4405,
    "example": 237,
    "start_line": 4039,
    "end_line": 4045,
    "section": "List items"
  },
  {
    "markdown": "003. ok\n",
    "html": "<ol start=\"3\">\n<li>ok</li>\n</ol>\n",
    "example": 268,
    "start_line": 4408,
    "end_line": 4414,
    "example": 238,
    "start_line": 4048,
    "end_line": 4054,
    "section": "List items"
  },
  {
    "markdown": "-1. not ok\n",
    "html": "<p>-1. not ok</p>\n",
    "example": 269,
    "start_line": 4419,
    "end_line": 4423,
    "example": 239,
    "start_line": 4059,
    "end_line": 4063,
    "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": 270,
    "start_line": 4442,
    "end_line": 4454,
    "example": 240,
    "start_line": 4082,
    "end_line": 4094,
    "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": 271,
    "start_line": 4459,
    "end_line": 4471,
    "example": 241,
    "start_line": 4099,
    "end_line": 4111,
    "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": 272,
    "start_line": 4478,
    "end_line": 4490,
    "example": 242,
    "start_line": 4118,
    "end_line": 4130,
    "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": 273,
    "start_line": 4493,
    "end_line": 4509,
    "example": 243,
    "start_line": 4133,
    "end_line": 4149,
    "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": 274,
    "start_line": 4515,
    "end_line": 4531,
    "example": 244,
    "start_line": 4155,
    "end_line": 4171,
    "section": "List items"
  },
  {
    "markdown": "   foo\n\nbar\n",
    "html": "<p>foo</p>\n<p>bar</p>\n",
    "example": 275,
    "start_line": 4542,
    "end_line": 4549,
    "example": 245,
    "start_line": 4182,
    "end_line": 4189,
    "section": "List items"
  },
  {
    "markdown": "-    foo\n\n  bar\n",
    "html": "<ul>\n<li>foo</li>\n</ul>\n<p>bar</p>\n",
    "example": 276,
    "start_line": 4552,
    "end_line": 4561,
    "example": 246,
    "start_line": 4192,
    "end_line": 4201,
    "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": 277,
    "start_line": 4569,
    "end_line": 4580,
    "example": 247,
    "start_line": 4209,
    "end_line": 4220,
    "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": 278,
    "start_line": 4596,
    "end_line": 4617,
    "example": 248,
    "start_line": 4237,
    "end_line": 4258,
    "section": "List items"
  },
  {
    "markdown": "-   \n  foo\n",
    "html": "<ul>\n<li>foo</li>\n</ul>\n",
    "example": 279,
    "start_line": 4622,
    "end_line": 4629,
    "example": 249,
    "start_line": 4263,
    "end_line": 4270,
    "section": "List items"
  },
  {
    "markdown": "-\n\n  foo\n",
    "html": "<ul>\n<li></li>\n</ul>\n<p>foo</p>\n",
    "example": 280,
    "start_line": 4636,
    "end_line": 4645,
    "example": 250,
    "start_line": 4277,
    "end_line": 4286,
    "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": 281,
    "start_line": 4650,
    "end_line": 4660,
    "example": 251,
    "start_line": 4291,
    "end_line": 4301,
    "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": 282,
    "start_line": 4665,
    "end_line": 4675,
    "example": 252,
    "start_line": 4306,
    "end_line": 4316,
    "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": 283,
    "start_line": 4680,
    "end_line": 4690,
    "example": 253,
    "start_line": 4321,
    "end_line": 4331,
    "section": "List items"
  },
  {
    "markdown": "*\n",
    "html": "<ul>\n<li></li>\n</ul>\n",
    "example": 284,
    "start_line": 4695,
    "end_line": 4701,
    "example": 254,
    "start_line": 4336,
    "end_line": 4342,
    "section": "List items"
  },
  {
    "markdown": "foo\n*\n\nfoo\n1.\n",
    "html": "<p>foo\n*</p>\n<p>foo\n1.</p>\n",
    "example": 285,
    "start_line": 4705,
    "end_line": 4716,
    "example": 255,
    "start_line": 4346,
    "end_line": 4357,
    "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": 286,
    "start_line": 4727,
    "end_line": 4746,
    "example": 256,
    "start_line": 4368,
    "end_line": 4387,
    "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": 287,
    "start_line": 4751,
    "end_line": 4770,
    "example": 257,
    "start_line": 4392,
    "end_line": 4411,
    "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": 288,
    "start_line": 4775,
    "end_line": 4794,
    "example": 258,
    "start_line": 4416,
    "end_line": 4435,
    "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": 289,
    "start_line": 4799,
    "end_line": 4814,
    "example": 259,
    "start_line": 4440,
    "end_line": 4455,
    "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": 290,
    "start_line": 4829,
    "end_line": 4848,
    "example": 260,
    "start_line": 4470,
    "end_line": 4489,
    "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": 291,
    "start_line": 4853,
    "end_line": 4861,
    "example": 261,
    "start_line": 4494,
    "end_line": 4502,
    "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": 292,
    "start_line": 4866,
    "end_line": 4880,
    "example": 262,
    "start_line": 4507,
    "end_line": 4521,
    "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": 293,
    "start_line": 4883,
    "end_line": 4897,
    "example": 263,
    "start_line": 4524,
    "end_line": 4538,
    "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": 294,
    "start_line": 4911,
    "end_line": 4932,
    "example": 264,
    "start_line": 4552,
    "end_line": 4573,
    "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": 295,
    "start_line": 4937,
    "end_line": 4949,
    "example": 265,
    "start_line": 4578,
    "end_line": 4590,
    "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": 296,
    "start_line": 4954,
    "end_line": 4965,
    "example": 266,
    "start_line": 4595,
    "end_line": 4606,
    "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": 297,
    "start_line": 4970,
    "end_line": 4980,
    "example": 267,
    "start_line": 4611,
    "end_line": 4621,
    "section": "List items"
  },
  {
    "markdown": "- - foo\n",
    "html": "<ul>\n<li>\n<ul>\n<li>foo</li>\n</ul>\n</li>\n</ul>\n",
    "example": 298,
    "start_line": 4985,
    "end_line": 4995,
    "example": 268,
    "start_line": 4626,
    "end_line": 4636,
    "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": 299,
    "start_line": 4998,
    "end_line": 5012,
    "example": 269,
    "start_line": 4639,
    "end_line": 4653,
    "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": 300,
    "start_line": 5017,
    "end_line": 5031,
    "example": 270,
    "start_line": 4658,
    "end_line": 4672,
    "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": 301,
    "start_line": 5253,
    "end_line": 5265,
    "example": 271,
    "start_line": 4894,
    "end_line": 4906,
    "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": 302,
    "start_line": 5268,
    "end_line": 5280,
    "example": 272,
    "start_line": 4909,
    "end_line": 4921,
    "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": 303,
    "start_line": 5287,
    "end_line": 5297,
    "example": 273,
    "start_line": 4928,
    "end_line": 4938,
    "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": 304,
    "start_line": 5364,
    "end_line": 5370,
    "example": 274,
    "start_line": 5005,
    "end_line": 5011,
    "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": 305,
    "start_line": 5374,
    "end_line": 5382,
    "example": 275,
    "start_line": 5015,
    "end_line": 5023,
    "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": 306,
    "start_line": 5388,
    "end_line": 5407,
    "example": 276,
    "start_line": 5029,
    "end_line": 5048,
    "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": 307,
    "start_line": 5409,
    "end_line": 5431,
    "example": 277,
    "start_line": 5050,
    "end_line": 5072,
    "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": 308,
    "start_line": 5439,
    "end_line": 5457,
    "example": 278,
    "start_line": 5080,
    "end_line": 5098,
    "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": 309,
    "start_line": 5460,
    "end_line": 5483,
    "example": 279,
    "start_line": 5101,
    "end_line": 5124,
    "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": 310,
    "start_line": 5491,
    "end_line": 5509,
    "example": 280,
    "start_line": 5132,
    "end_line": 5150,
    "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": 311,
    "start_line": 5512,
    "end_line": 5530,
    "example": 281,
    "start_line": 5153,
    "end_line": 5171,
    "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": 312,
    "start_line": 5536,
    "end_line": 5550,
    "example": 282,
    "start_line": 5177,
    "end_line": 5191,
    "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": 313,
    "start_line": 5556,
    "end_line": 5573,
    "example": 283,
    "start_line": 5197,
    "end_line": 5214,
    "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": 314,
    "start_line": 5579,
    "end_line": 5596,
    "example": 284,
    "start_line": 5220,
    "end_line": 5237,
    "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": 315,
    "start_line": 5601,
    "end_line": 5616,
    "example": 285,
    "start_line": 5242,
    "end_line": 5257,
    "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": 316,
    "start_line": 5623,
    "end_line": 5642,
    "example": 286,
    "start_line": 5264,
    "end_line": 5283,
    "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": 317,
    "start_line": 5645,
    "end_line": 5663,
    "example": 287,
    "start_line": 5286,
    "end_line": 5304,
    "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": 318,
    "start_line": 5668,
    "end_line": 5687,
    "example": 288,
    "start_line": 5309,
    "end_line": 5328,
    "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": 319,
    "start_line": 5694,
    "end_line": 5712,
    "example": 289,
    "start_line": 5335,
    "end_line": 5353,
    "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": 320,
    "start_line": 5718,
    "end_line": 5732,
    "example": 290,
    "start_line": 5359,
    "end_line": 5373,
    "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": 321,
    "start_line": 5738,
    "end_line": 5756,
    "example": 291,
    "start_line": 5379,
    "end_line": 5397,
    "section": "Lists"
  },
  {
    "markdown": "- a\n",
    "html": "<ul>\n<li>a</li>\n</ul>\n",
    "example": 322,
    "start_line": 5761,
    "end_line": 5767,
    "example": 292,
    "start_line": 5402,
    "end_line": 5408,
    "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": 323,
    "start_line": 5770,
    "end_line": 5781,
    "example": 293,
    "start_line": 5411,
    "end_line": 5422,
    "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": 324,
    "start_line": 5787,
    "end_line": 5801,
    "example": 294,
    "start_line": 5428,
    "end_line": 5442,
    "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": 325,
    "start_line": 5806,
    "end_line": 5821,
    "example": 295,
    "start_line": 5447,
    "end_line": 5462,
    "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": 326,
    "start_line": 5824,
    "end_line": 5849,
    "example": 296,
    "start_line": 5465,
    "end_line": 5490,
    "section": "Lists"
  },
  {
    "markdown": "`hi`lo`\n",
    "html": "<p><code>hi</code>lo`</p>\n",
    "example": 327,
    "start_line": 5858,
    "end_line": 5862,
    "example": 297,
    "start_line": 5499,
    "end_line": 5503,
    "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": 5890,
    "end_line": 5894,
    "start_line": 5870,
    "end_line": 5874,
    "section": "Code spans"
  },
  {
    "markdown": "`` foo ` bar ``\n",
    "html": "<p><code>foo ` bar</code></p>\n",
    "example": 329,
    "start_line": 5901,
    "end_line": 5905,
    "start_line": 5881,
    "end_line": 5885,
    "section": "Code spans"
  },
  {
    "markdown": "` `` `\n",
    "html": "<p><code>``</code></p>\n",
    "example": 330,
    "start_line": 5911,
    "end_line": 5915,
    "start_line": 5891,
    "end_line": 5895,
    "section": "Code spans"
  },
  {
    "markdown": "`  ``  `\n",
    "html": "<p><code> `` </code></p>\n",
    "example": 331,
    "start_line": 5919,
    "end_line": 5923,
    "start_line": 5899,
    "end_line": 5903,
    "section": "Code spans"
  },
  {
    "markdown": "` a`\n",
    "html": "<p><code> a</code></p>\n",
    "example": 332,
    "start_line": 5928,
    "end_line": 5932,
    "start_line": 5908,
    "end_line": 5912,
    "section": "Code spans"
  },
  {
    "markdown": "` b `\n",
    "html": "<p><code> b </code></p>\n",
    "example": 333,
    "start_line": 5937,
    "end_line": 5941,
    "start_line": 5917,
    "end_line": 5921,
    "section": "Code spans"
  },
  {
    "markdown": "` `\n`  `\n",
    "html": "<p><code> </code>\n<code>  </code></p>\n",
    "example": 334,
    "start_line": 5945,
    "end_line": 5951,
    "start_line": 5925,
    "end_line": 5931,
    "section": "Code spans"
  },
  {
    "markdown": "``\nfoo\nbar  \nbaz\n``\n",
    "html": "<p><code>foo bar   baz</code></p>\n",
    "example": 335,
    "start_line": 5956,
    "end_line": 5964,
    "start_line": 5936,
    "end_line": 5944,
    "section": "Code spans"
  },
  {
    "markdown": "``\nfoo \n``\n",
    "html": "<p><code>foo </code></p>\n",
    "example": 336,
    "start_line": 5966,
    "end_line": 5972,
    "start_line": 5946,
    "end_line": 5952,
    "section": "Code spans"
  },
  {
    "markdown": "`foo   bar \nbaz`\n",
    "html": "<p><code>foo   bar  baz</code></p>\n",
    "example": 337,
    "start_line": 5977,
    "end_line": 5982,
    "start_line": 5957,
    "end_line": 5962,
    "section": "Code spans"
  },
  {
    "markdown": "`foo\\`bar`\n",
    "html": "<p><code>foo\\</code>bar`</p>\n",
    "example": 338,
    "start_line": 5994,
    "end_line": 5998,
    "start_line": 5974,
    "end_line": 5978,
    "section": "Code spans"
  },
  {
    "markdown": "``foo`bar``\n",
    "html": "<p><code>foo`bar</code></p>\n",
    "example": 339,
    "start_line": 6005,
    "end_line": 6009,
    "start_line": 5985,
    "end_line": 5989,
    "section": "Code spans"
  },
  {
    "markdown": "` foo `` bar `\n",
    "html": "<p><code>foo `` bar</code></p>\n",
    "example": 340,
    "start_line": 6011,
    "end_line": 6015,
    "start_line": 5991,
    "end_line": 5995,
    "section": "Code spans"
  },
  {
    "markdown": "*foo`*`\n",
    "html": "<p>*foo<code>*</code></p>\n",
    "example": 341,
    "start_line": 6023,
    "end_line": 6027,
    "start_line": 6003,
    "end_line": 6007,
    "section": "Code spans"
  },
  {
    "markdown": "[not a `link](/foo`)\n",
    "html": "<p>[not a <code>link](/foo</code>)</p>\n",
    "example": 342,
    "start_line": 6032,
    "end_line": 6036,
    "start_line": 6012,
    "end_line": 6016,
    "section": "Code spans"
  },
  {
    "markdown": "`<a href=\"`\">`\n",
    "html": "<p><code>&lt;a href=&quot;</code>&quot;&gt;`</p>\n",
    "example": 343,
    "start_line": 6042,
    "end_line": 6046,
    "start_line": 6022,
    "end_line": 6026,
    "section": "Code spans"
  },
  {
    "markdown": "<a href=\"`\">`\n",
    "html": "<p><a href=\"`\">`</p>\n",
    "example": 344,
    "start_line": 6051,
    "end_line": 6055,
    "start_line": 6031,
    "end_line": 6035,
    "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": 6060,
    "end_line": 6064,
    "start_line": 6040,
    "end_line": 6044,
    "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": 6069,
    "end_line": 6073,
    "start_line": 6049,
    "end_line": 6053,
    "section": "Code spans"
  },
  {
    "markdown": "```foo``\n",
    "html": "<p>```foo``</p>\n",
    "example": 347,
    "start_line": 6079,
    "end_line": 6083,
    "start_line": 6059,
    "end_line": 6063,
    "section": "Code spans"
  },
  {
    "markdown": "`foo\n",
    "html": "<p>`foo</p>\n",
    "example": 348,
    "start_line": 6086,
    "end_line": 6090,
    "start_line": 6066,
    "end_line": 6070,
    "section": "Code spans"
  },
  {
    "markdown": "`foo``bar``\n",
    "html": "<p>`foo<code>bar</code></p>\n",
    "example": 349,
    "start_line": 6095,
    "end_line": 6099,
    "start_line": 6075,
    "end_line": 6079,
    "section": "Code spans"
  },
  {
    "markdown": "*foo bar*\n",
    "html": "<p><em>foo bar</em></p>\n",
    "example": 350,
    "start_line": 6312,
    "end_line": 6316,
    "start_line": 6292,
    "end_line": 6296,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a * foo bar*\n",
    "html": "<p>a * foo bar*</p>\n",
    "example": 351,
    "start_line": 6322,
    "end_line": 6326,
    "start_line": 6302,
    "end_line": 6306,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a*\"foo\"*\n",
    "html": "<p>a*&quot;foo&quot;*</p>\n",
    "example": 352,
    "start_line": 6333,
    "end_line": 6337,
    "start_line": 6313,
    "end_line": 6317,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "* a *\n",
    "html": "<p>* a *</p>\n",
    "example": 353,
    "start_line": 6342,
    "end_line": 6346,
    "start_line": 6322,
    "end_line": 6326,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo*bar*\n",
    "html": "<p>foo<em>bar</em></p>\n",
    "example": 354,
    "start_line": 6351,
    "end_line": 6355,
    "start_line": 6331,
    "end_line": 6335,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "5*6*78\n",
    "html": "<p>5<em>6</em>78</p>\n",
    "example": 355,
    "start_line": 6358,
    "end_line": 6362,
    "start_line": 6338,
    "end_line": 6342,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo bar_\n",
    "html": "<p><em>foo bar</em></p>\n",
    "example": 356,
    "start_line": 6367,
    "end_line": 6371,
    "start_line": 6347,
    "end_line": 6351,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_ foo bar_\n",
    "html": "<p>_ foo bar_</p>\n",
    "example": 357,
    "start_line": 6377,
    "end_line": 6381,
    "start_line": 6357,
    "end_line": 6361,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a_\"foo\"_\n",
    "html": "<p>a_&quot;foo&quot;_</p>\n",
    "example": 358,
    "start_line": 6387,
    "end_line": 6391,
    "start_line": 6367,
    "end_line": 6371,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo_bar_\n",
    "html": "<p>foo_bar_</p>\n",
    "example": 359,
    "start_line": 6396,
    "end_line": 6400,
    "start_line": 6376,
    "end_line": 6380,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "5_6_78\n",
    "html": "<p>5_6_78</p>\n",
    "example": 360,
    "start_line": 6403,
    "end_line": 6407,
    "start_line": 6383,
    "end_line": 6387,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "пристаням_стремятся_\n",
    "html": "<p>пристаням_стремятся_</p>\n",
    "example": 361,
    "start_line": 6410,
    "end_line": 6414,
    "start_line": 6390,
    "end_line": 6394,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "aa_\"bb\"_cc\n",
    "html": "<p>aa_&quot;bb&quot;_cc</p>\n",
    "example": 362,
    "start_line": 6420,
    "end_line": 6424,
    "start_line": 6400,
    "end_line": 6404,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo-_(bar)_\n",
    "html": "<p>foo-<em>(bar)</em></p>\n",
    "example": 363,
    "start_line": 6431,
    "end_line": 6435,
    "start_line": 6411,
    "end_line": 6415,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo*\n",
    "html": "<p>_foo*</p>\n",
    "example": 364,
    "start_line": 6443,
    "end_line": 6447,
    "start_line": 6423,
    "end_line": 6427,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo bar *\n",
    "html": "<p>*foo bar *</p>\n",
    "example": 365,
    "start_line": 6453,
    "end_line": 6457,
    "start_line": 6433,
    "end_line": 6437,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo bar\n*\n",
    "html": "<p>*foo bar\n*</p>\n",
    "example": 366,
    "start_line": 6462,
    "end_line": 6468,
    "start_line": 6442,
    "end_line": 6448,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*(*foo)\n",
    "html": "<p>*(*foo)</p>\n",
    "example": 367,
    "start_line": 6475,
    "end_line": 6479,
    "start_line": 6455,
    "end_line": 6459,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*(*foo*)*\n",
    "html": "<p><em>(<em>foo</em>)</em></p>\n",
    "example": 368,
    "start_line": 6485,
    "end_line": 6489,
    "start_line": 6465,
    "end_line": 6469,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo*bar\n",
    "html": "<p><em>foo</em>bar</p>\n",
    "example": 369,
    "start_line": 6494,
    "end_line": 6498,
    "start_line": 6474,
    "end_line": 6478,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo bar _\n",
    "html": "<p>_foo bar _</p>\n",
    "example": 370,
    "start_line": 6507,
    "end_line": 6511,
    "start_line": 6487,
    "end_line": 6491,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(_foo)\n",
    "html": "<p>_(_foo)</p>\n",
    "example": 371,
    "start_line": 6517,
    "end_line": 6521,
    "start_line": 6497,
    "end_line": 6501,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(_foo_)_\n",
    "html": "<p><em>(<em>foo</em>)</em></p>\n",
    "example": 372,
    "start_line": 6526,
    "end_line": 6530,
    "start_line": 6506,
    "end_line": 6510,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo_bar\n",
    "html": "<p>_foo_bar</p>\n",
    "example": 373,
    "start_line": 6535,
    "end_line": 6539,
    "start_line": 6515,
    "end_line": 6519,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_пристаням_стремятся\n",
    "html": "<p>_пристаням_стремятся</p>\n",
    "example": 374,
    "start_line": 6542,
    "end_line": 6546,
    "start_line": 6522,
    "end_line": 6526,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo_bar_baz_\n",
    "html": "<p><em>foo_bar_baz</em></p>\n",
    "example": 375,
    "start_line": 6549,
    "end_line": 6553,
    "start_line": 6529,
    "end_line": 6533,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(bar)_.\n",
    "html": "<p><em>(bar)</em>.</p>\n",
    "example": 376,
    "start_line": 6560,
    "end_line": 6564,
    "start_line": 6540,
    "end_line": 6544,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo bar**\n",
    "html": "<p><strong>foo bar</strong></p>\n",
    "example": 377,
    "start_line": 6569,
    "end_line": 6573,
    "start_line": 6549,
    "end_line": 6553,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "** foo bar**\n",
    "html": "<p>** foo bar**</p>\n",
    "example": 378,
    "start_line": 6579,
    "end_line": 6583,
    "start_line": 6559,
    "end_line": 6563,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a**\"foo\"**\n",
    "html": "<p>a**&quot;foo&quot;**</p>\n",
    "example": 379,
    "start_line": 6590,
    "end_line": 6594,
    "start_line": 6570,
    "end_line": 6574,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo**bar**\n",
    "html": "<p>foo<strong>bar</strong></p>\n",
    "example": 380,
    "start_line": 6599,
    "end_line": 6603,
    "start_line": 6579,
    "end_line": 6583,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo bar__\n",
    "html": "<p><strong>foo bar</strong></p>\n",
    "example": 381,
    "start_line": 6608,
    "end_line": 6612,
    "start_line": 6588,
    "end_line": 6592,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__ foo bar__\n",
    "html": "<p>__ foo bar__</p>\n",
    "example": 382,
    "start_line": 6618,
    "end_line": 6622,
    "start_line": 6598,
    "end_line": 6602,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__\nfoo bar__\n",
    "html": "<p>__\nfoo bar__</p>\n",
    "example": 383,
    "start_line": 6626,
    "end_line": 6632,
    "start_line": 6606,
    "end_line": 6612,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "a__\"foo\"__\n",
    "html": "<p>a__&quot;foo&quot;__</p>\n",
    "example": 384,
    "start_line": 6638,
    "end_line": 6642,
    "start_line": 6618,
    "end_line": 6622,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo__bar__\n",
    "html": "<p>foo__bar__</p>\n",
    "example": 385,
    "start_line": 6647,
    "end_line": 6651,
    "start_line": 6627,
    "end_line": 6631,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "5__6__78\n",
    "html": "<p>5__6__78</p>\n",
    "example": 386,
    "start_line": 6654,
    "end_line": 6658,
    "start_line": 6634,
    "end_line": 6638,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "пристаням__стремятся__\n",
    "html": "<p>пристаням__стремятся__</p>\n",
    "example": 387,
    "start_line": 6661,
    "end_line": 6665,
    "start_line": 6641,
    "end_line": 6645,
    "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": 6668,
    "end_line": 6672,
    "start_line": 6648,
    "end_line": 6652,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo-__(bar)__\n",
    "html": "<p>foo-<strong>(bar)</strong></p>\n",
    "example": 389,
    "start_line": 6679,
    "end_line": 6683,
    "start_line": 6659,
    "end_line": 6663,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo bar **\n",
    "html": "<p>**foo bar **</p>\n",
    "example": 390,
    "start_line": 6692,
    "end_line": 6696,
    "start_line": 6672,
    "end_line": 6676,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**(**foo)\n",
    "html": "<p>**(**foo)</p>\n",
    "example": 391,
    "start_line": 6705,
    "end_line": 6709,
    "start_line": 6685,
    "end_line": 6689,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*(**foo**)*\n",
    "html": "<p><em>(<strong>foo</strong>)</em></p>\n",
    "example": 392,
    "start_line": 6715,
    "end_line": 6719,
    "start_line": 6695,
    "end_line": 6699,
    "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": 6722,
    "end_line": 6728,
    "start_line": 6702,
    "end_line": 6708,
    "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": 6731,
    "end_line": 6735,
    "start_line": 6711,
    "end_line": 6715,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo**bar\n",
    "html": "<p><strong>foo</strong>bar</p>\n",
    "example": 395,
    "start_line": 6740,
    "end_line": 6744,
    "start_line": 6720,
    "end_line": 6724,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo bar __\n",
    "html": "<p>__foo bar __</p>\n",
    "example": 396,
    "start_line": 6752,
    "end_line": 6756,
    "start_line": 6732,
    "end_line": 6736,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__(__foo)\n",
    "html": "<p>__(__foo)</p>\n",
    "example": 397,
    "start_line": 6762,
    "end_line": 6766,
    "start_line": 6742,
    "end_line": 6746,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_(__foo__)_\n",
    "html": "<p><em>(<strong>foo</strong>)</em></p>\n",
    "example": 398,
    "start_line": 6772,
    "end_line": 6776,
    "start_line": 6752,
    "end_line": 6756,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo__bar\n",
    "html": "<p>__foo__bar</p>\n",
    "example": 399,
    "start_line": 6781,
    "end_line": 6785,
    "start_line": 6761,
    "end_line": 6765,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__пристаням__стремятся\n",
    "html": "<p>__пристаням__стремятся</p>\n",
    "example": 400,
    "start_line": 6788,
    "end_line": 6792,
    "start_line": 6768,
    "end_line": 6772,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo__bar__baz__\n",
    "html": "<p><strong>foo__bar__baz</strong></p>\n",
    "example": 401,
    "start_line": 6795,
    "end_line": 6799,
    "start_line": 6775,
    "end_line": 6779,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__(bar)__.\n",
    "html": "<p><strong>(bar)</strong>.</p>\n",
    "example": 402,
    "start_line": 6806,
    "end_line": 6810,
    "start_line": 6786,
    "end_line": 6790,
    "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": 6818,
    "end_line": 6822,
    "start_line": 6798,
    "end_line": 6802,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo\nbar*\n",
    "html": "<p><em>foo\nbar</em></p>\n",
    "example": 404,
    "start_line": 6825,
    "end_line": 6831,
    "start_line": 6805,
    "end_line": 6811,
    "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": 6837,
    "end_line": 6841,
    "start_line": 6817,
    "end_line": 6821,
    "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": 6844,
    "end_line": 6848,
    "start_line": 6824,
    "end_line": 6828,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo_ bar_\n",
    "html": "<p><em><em>foo</em> bar</em></p>\n",
    "example": 407,
    "start_line": 6851,
    "end_line": 6855,
    "start_line": 6831,
    "end_line": 6835,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo *bar**\n",
    "html": "<p><em>foo <em>bar</em></em></p>\n",
    "example": 408,
    "start_line": 6858,
    "end_line": 6862,
    "start_line": 6838,
    "end_line": 6842,
    "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": 6865,
    "end_line": 6869,
    "start_line": 6845,
    "end_line": 6849,
    "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": 6871,
    "end_line": 6875,
    "start_line": 6851,
    "end_line": 6855,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo**bar*\n",
    "html": "<p><em>foo**bar</em></p>\n",
    "example": 411,
    "start_line": 6895,
    "end_line": 6899,
    "start_line": 6875,
    "end_line": 6879,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo** bar*\n",
    "html": "<p><em><strong>foo</strong> bar</em></p>\n",
    "example": 412,
    "start_line": 6908,
    "end_line": 6912,
    "start_line": 6888,
    "end_line": 6892,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo **bar***\n",
    "html": "<p><em>foo <strong>bar</strong></em></p>\n",
    "example": 413,
    "start_line": 6915,
    "end_line": 6919,
    "start_line": 6895,
    "end_line": 6899,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo**bar***\n",
    "html": "<p><em>foo<strong>bar</strong></em></p>\n",
    "example": 414,
    "start_line": 6922,
    "end_line": 6926,
    "start_line": 6902,
    "end_line": 6906,
    "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": 6933,
    "end_line": 6937,
    "start_line": 6913,
    "end_line": 6917,
    "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": 6939,
    "end_line": 6943,
    "start_line": 6919,
    "end_line": 6923,
    "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": 6948,
    "end_line": 6952,
    "start_line": 6928,
    "end_line": 6932,
    "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": 6955,
    "end_line": 6959,
    "start_line": 6935,
    "end_line": 6939,
    "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": 6964,
    "end_line": 6968,
    "start_line": 6944,
    "end_line": 6948,
    "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": 6971,
    "end_line": 6975,
    "start_line": 6951,
    "end_line": 6955,
    "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": 6984,
    "end_line": 6988,
    "start_line": 6964,
    "end_line": 6968,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo\nbar**\n",
    "html": "<p><strong>foo\nbar</strong></p>\n",
    "example": 422,
    "start_line": 6991,
    "end_line": 6997,
    "start_line": 6971,
    "end_line": 6977,
    "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": 7003,
    "end_line": 7007,
    "start_line": 6983,
    "end_line": 6987,
    "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": 7010,
    "end_line": 7014,
    "start_line": 6990,
    "end_line": 6994,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "____foo__ bar__\n",
    "html": "<p><strong><strong>foo</strong> bar</strong></p>\n",
    "example": 425,
    "start_line": 7017,
    "end_line": 7021,
    "start_line": 6997,
    "end_line": 7001,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo **bar****\n",
    "html": "<p><strong>foo <strong>bar</strong></strong></p>\n",
    "example": 426,
    "start_line": 7024,
    "end_line": 7028,
    "start_line": 7004,
    "end_line": 7008,
    "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": 7031,
    "end_line": 7035,
    "start_line": 7011,
    "end_line": 7015,
    "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": 7038,
    "end_line": 7042,
    "start_line": 7018,
    "end_line": 7022,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo* bar**\n",
    "html": "<p><strong><em>foo</em> bar</strong></p>\n",
    "example": 429,
    "start_line": 7045,
    "end_line": 7049,
    "start_line": 7025,
    "end_line": 7029,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo *bar***\n",
    "html": "<p><strong>foo <em>bar</em></strong></p>\n",
    "example": 430,
    "start_line": 7052,
    "end_line": 7056,
    "start_line": 7032,
    "end_line": 7036,
    "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": 7061,
    "end_line": 7067,
    "start_line": 7041,
    "end_line": 7047,
    "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": 7070,
    "end_line": 7074,
    "start_line": 7050,
    "end_line": 7054,
    "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": 7079,
    "end_line": 7083,
    "start_line": 7059,
    "end_line": 7063,
    "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": 7086,
    "end_line": 7090,
    "start_line": 7066,
    "end_line": 7070,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo ***\n",
    "html": "<p>foo ***</p>\n",
    "example": 435,
    "start_line": 7096,
    "end_line": 7100,
    "start_line": 7076,
    "end_line": 7080,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo *\\**\n",
    "html": "<p>foo <em>*</em></p>\n",
    "example": 436,
    "start_line": 7103,
    "end_line": 7107,
    "start_line": 7083,
    "end_line": 7087,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo *_*\n",
    "html": "<p>foo <em>_</em></p>\n",
    "example": 437,
    "start_line": 7110,
    "end_line": 7114,
    "start_line": 7090,
    "end_line": 7094,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo *****\n",
    "html": "<p>foo *****</p>\n",
    "example": 438,
    "start_line": 7117,
    "end_line": 7121,
    "start_line": 7097,
    "end_line": 7101,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo **\\***\n",
    "html": "<p>foo <strong>*</strong></p>\n",
    "example": 439,
    "start_line": 7124,
    "end_line": 7128,
    "start_line": 7104,
    "end_line": 7108,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo **_**\n",
    "html": "<p>foo <strong>_</strong></p>\n",
    "example": 440,
    "start_line": 7131,
    "end_line": 7135,
    "start_line": 7111,
    "end_line": 7115,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo*\n",
    "html": "<p>*<em>foo</em></p>\n",
    "example": 441,
    "start_line": 7142,
    "end_line": 7146,
    "start_line": 7122,
    "end_line": 7126,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo**\n",
    "html": "<p><em>foo</em>*</p>\n",
    "example": 442,
    "start_line": 7149,
    "end_line": 7153,
    "start_line": 7129,
    "end_line": 7133,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo**\n",
    "html": "<p>*<strong>foo</strong></p>\n",
    "example": 443,
    "start_line": 7156,
    "end_line": 7160,
    "start_line": 7136,
    "end_line": 7140,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "****foo*\n",
    "html": "<p>***<em>foo</em></p>\n",
    "example": 444,
    "start_line": 7163,
    "end_line": 7167,
    "start_line": 7143,
    "end_line": 7147,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo***\n",
    "html": "<p><strong>foo</strong>*</p>\n",
    "example": 445,
    "start_line": 7170,
    "end_line": 7174,
    "start_line": 7150,
    "end_line": 7154,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo****\n",
    "html": "<p><em>foo</em>***</p>\n",
    "example": 446,
    "start_line": 7177,
    "end_line": 7181,
    "start_line": 7157,
    "end_line": 7161,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo ___\n",
    "html": "<p>foo ___</p>\n",
    "example": 447,
    "start_line": 7187,
    "end_line": 7191,
    "start_line": 7167,
    "end_line": 7171,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo _\\__\n",
    "html": "<p>foo <em>_</em></p>\n",
    "example": 448,
    "start_line": 7194,
    "end_line": 7198,
    "start_line": 7174,
    "end_line": 7178,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo _*_\n",
    "html": "<p>foo <em>*</em></p>\n",
    "example": 449,
    "start_line": 7201,
    "end_line": 7205,
    "start_line": 7181,
    "end_line": 7185,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo _____\n",
    "html": "<p>foo _____</p>\n",
    "example": 450,
    "start_line": 7208,
    "end_line": 7212,
    "start_line": 7188,
    "end_line": 7192,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo __\\___\n",
    "html": "<p>foo <strong>_</strong></p>\n",
    "example": 451,
    "start_line": 7215,
    "end_line": 7219,
    "start_line": 7195,
    "end_line": 7199,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "foo __*__\n",
    "html": "<p>foo <strong>*</strong></p>\n",
    "example": 452,
    "start_line": 7222,
    "end_line": 7226,
    "start_line": 7202,
    "end_line": 7206,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo_\n",
    "html": "<p>_<em>foo</em></p>\n",
    "example": 453,
    "start_line": 7229,
    "end_line": 7233,
    "start_line": 7209,
    "end_line": 7213,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo__\n",
    "html": "<p><em>foo</em>_</p>\n",
    "example": 454,
    "start_line": 7240,
    "end_line": 7244,
    "start_line": 7220,
    "end_line": 7224,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "___foo__\n",
    "html": "<p>_<strong>foo</strong></p>\n",
    "example": 455,
    "start_line": 7247,
    "end_line": 7251,
    "start_line": 7227,
    "end_line": 7231,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "____foo_\n",
    "html": "<p>___<em>foo</em></p>\n",
    "example": 456,
    "start_line": 7254,
    "end_line": 7258,
    "start_line": 7234,
    "end_line": 7238,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo___\n",
    "html": "<p><strong>foo</strong>_</p>\n",
    "example": 457,
    "start_line": 7261,
    "end_line": 7265,
    "start_line": 7241,
    "end_line": 7245,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo____\n",
    "html": "<p><em>foo</em>___</p>\n",
    "example": 458,
    "start_line": 7268,
    "end_line": 7272,
    "start_line": 7248,
    "end_line": 7252,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo**\n",
    "html": "<p><strong>foo</strong></p>\n",
    "example": 459,
    "start_line": 7278,
    "end_line": 7282,
    "start_line": 7258,
    "end_line": 7262,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*_foo_*\n",
    "html": "<p><em><em>foo</em></em></p>\n",
    "example": 460,
    "start_line": 7285,
    "end_line": 7289,
    "start_line": 7265,
    "end_line": 7269,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__foo__\n",
    "html": "<p><strong>foo</strong></p>\n",
    "example": 461,
    "start_line": 7292,
    "end_line": 7296,
    "start_line": 7272,
    "end_line": 7276,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_*foo*_\n",
    "html": "<p><em><em>foo</em></em></p>\n",
    "example": 462,
    "start_line": 7299,
    "end_line": 7303,
    "start_line": 7279,
    "end_line": 7283,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "****foo****\n",
    "html": "<p><strong><strong>foo</strong></strong></p>\n",
    "example": 463,
    "start_line": 7309,
    "end_line": 7313,
    "start_line": 7289,
    "end_line": 7293,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "____foo____\n",
    "html": "<p><strong><strong>foo</strong></strong></p>\n",
    "example": 464,
    "start_line": 7316,
    "end_line": 7320,
    "start_line": 7296,
    "end_line": 7300,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "******foo******\n",
    "html": "<p><strong><strong><strong>foo</strong></strong></strong></p>\n",
    "example": 465,
    "start_line": 7327,
    "end_line": 7331,
    "start_line": 7307,
    "end_line": 7311,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "***foo***\n",
    "html": "<p><em><strong>foo</strong></em></p>\n",
    "example": 466,
    "start_line": 7336,
    "end_line": 7340,
    "start_line": 7316,
    "end_line": 7320,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_____foo_____\n",
    "html": "<p><em><strong><strong>foo</strong></strong></em></p>\n",
    "example": 467,
    "start_line": 7343,
    "end_line": 7347,
    "start_line": 7323,
    "end_line": 7327,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo _bar* baz_\n",
    "html": "<p><em>foo _bar</em> baz_</p>\n",
    "example": 468,
    "start_line": 7352,
    "end_line": 7356,
    "start_line": 7332,
    "end_line": 7336,
    "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": 7359,
    "end_line": 7363,
    "start_line": 7339,
    "end_line": 7343,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**foo **bar baz**\n",
    "html": "<p>**foo <strong>bar baz</strong></p>\n",
    "example": 470,
    "start_line": 7368,
    "end_line": 7372,
    "start_line": 7348,
    "end_line": 7352,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*foo *bar baz*\n",
    "html": "<p>*foo <em>bar baz</em></p>\n",
    "example": 471,
    "start_line": 7375,
    "end_line": 7379,
    "start_line": 7355,
    "end_line": 7359,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*[bar*](/url)\n",
    "html": "<p>*<a href=\"/url\">bar*</a></p>\n",
    "example": 472,
    "start_line": 7384,
    "end_line": 7388,
    "start_line": 7364,
    "end_line": 7368,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_foo [bar_](/url)\n",
    "html": "<p>_foo <a href=\"/url\">bar_</a></p>\n",
    "example": 473,
    "start_line": 7391,
    "end_line": 7395,
    "start_line": 7371,
    "end_line": 7375,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*<img src=\"foo\" title=\"*\"/>\n",
    "html": "<p>*<img src=\"foo\" title=\"*\"/></p>\n",
    "example": 474,
    "start_line": 7398,
    "end_line": 7402,
    "start_line": 7378,
    "end_line": 7382,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "**<a href=\"**\">\n",
    "html": "<p>**<a href=\"**\"></p>\n",
    "example": 475,
    "start_line": 7405,
    "end_line": 7409,
    "start_line": 7385,
    "end_line": 7389,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "__<a href=\"__\">\n",
    "html": "<p>__<a href=\"__\"></p>\n",
    "example": 476,
    "start_line": 7412,
    "end_line": 7416,
    "start_line": 7392,
    "end_line": 7396,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "*a `*`*\n",
    "html": "<p><em>a <code>*</code></em></p>\n",
    "example": 477,
    "start_line": 7419,
    "end_line": 7423,
    "start_line": 7399,
    "end_line": 7403,
    "section": "Emphasis and strong emphasis"
  },
  {
    "markdown": "_a `_`_\n",
    "html": "<p><em>a <code>_</code></em></p>\n",
    "example": 478,
    "start_line": 7426,
    "end_line": 7430,
    "start_line": 7406,
    "end_line": 7410,
    "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": 7433,
    "end_line": 7437,
    "start_line": 7413,
    "end_line": 7417,
    "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": 7440,
    "end_line": 7444,
    "start_line": 7420,
    "end_line": 7424,
    "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": 7528,
    "end_line": 7532,
    "start_line": 7503,
    "end_line": 7507,
    "section": "Links"
  },
  {
    "markdown": "[link](/uri)\n",
    "html": "<p><a href=\"/uri\">link</a></p>\n",
    "example": 482,
    "start_line": 7538,
    "end_line": 7542,
    "start_line": 7512,
    "end_line": 7516,
    "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": 484,
    "start_line": 7551,
    "end_line": 7555,
    "example": 483,
    "start_line": 7521,
    "end_line": 7525,
    "section": "Links"
  },
  {
    "markdown": "[link](<>)\n",
    "html": "<p><a href=\"\">link</a></p>\n",
    "example": 485,
    "start_line": 7558,
    "end_line": 7562,
    "example": 484,
    "start_line": 7528,
    "end_line": 7532,
    "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": 487,
    "start_line": 7574,
    "end_line": 7578,
    "example": 485,
    "start_line": 7537,
    "end_line": 7541,
    "section": "Links"
  },
  {
    "markdown": "[link](</my uri>)\n",
    "html": "<p><a href=\"/my%20uri\">link</a></p>\n",
    "example": 488,
    "start_line": 7580,
    "end_line": 7584,
    "example": 486,
    "start_line": 7543,
    "end_line": 7547,
    "section": "Links"
  },
  {
    "markdown": "[link](foo\nbar)\n",
    "html": "<p>[link](foo\nbar)</p>\n",
    "example": 489,
    "start_line": 7589,
    "end_line": 7595,
    "example": 487,
    "start_line": 7552,
    "end_line": 7558,
    "section": "Links"
  },
  {
    "markdown": "[link](<foo\nbar>)\n",
    "html": "<p>[link](<foo\nbar>)</p>\n",
    "example": 490,
    "start_line": 7597,
    "end_line": 7603,
    "example": 488,
    "start_line": 7560,
    "end_line": 7566,
    "section": "Links"
  },
  {
    "markdown": "[a](<b)c>)\n",
    "html": "<p><a href=\"b)c\">a</a></p>\n",
    "example": 491,
    "start_line": 7608,
    "end_line": 7612,
    "example": 489,
    "start_line": 7571,
    "end_line": 7575,
    "section": "Links"
  },
  {
    "markdown": "[link](<foo\\>)\n",
    "html": "<p>[link](&lt;foo&gt;)</p>\n",
    "example": 492,
    "start_line": 7616,
    "end_line": 7620,
    "example": 490,
    "start_line": 7579,
    "end_line": 7583,
    "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": 493,
    "start_line": 7625,
    "end_line": 7633,
    "example": 491,
    "start_line": 7588,
    "end_line": 7596,
    "section": "Links"
  },
  {
    "markdown": "[link](\\(foo\\))\n",
    "html": "<p><a href=\"(foo)\">link</a></p>\n",
    "example": 494,
    "start_line": 7637,
    "end_line": 7641,
    "example": 492,
    "start_line": 7600,
    "end_line": 7604,
    "section": "Links"
  },
  {
    "markdown": "[link](foo(and(bar)))\n",
    "html": "<p><a href=\"foo(and(bar))\">link</a></p>\n",
    "example": 495,
    "start_line": 7646,
    "end_line": 7650,
    "example": 493,
    "start_line": 7609,
    "end_line": 7613,
    "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": 497,
    "start_line": 7662,
    "end_line": 7666,
    "example": 494,
    "start_line": 7618,
    "end_line": 7622,
    "section": "Links"
  },
  {
    "markdown": "[link](<foo(and(bar)>)\n",
    "html": "<p><a href=\"foo(and(bar)\">link</a></p>\n",
    "example": 498,
    "start_line": 7669,
    "end_line": 7673,
    "example": 495,
    "start_line": 7625,
    "end_line": 7629,
    "section": "Links"
  },
  {
    "markdown": "[link](foo\\)\\:)\n",
    "html": "<p><a href=\"foo):\">link</a></p>\n",
    "example": 499,
    "start_line": 7679,
    "end_line": 7683,
    "example": 496,
    "start_line": 7635,
    "end_line": 7639,
    "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": 500,
    "start_line": 7688,
    "end_line": 7698,
    "example": 497,
    "start_line": 7644,
    "end_line": 7654,
    "section": "Links"
  },
  {
    "markdown": "[link](foo\\bar)\n",
    "html": "<p><a href=\"foo%5Cbar\">link</a></p>\n",
    "example": 501,
    "start_line": 7704,
    "end_line": 7708,
    "example": 498,
    "start_line": 7660,
    "end_line": 7664,
    "section": "Links"
  },
  {
    "markdown": "[link](foo%20b&auml;)\n",
    "html": "<p><a href=\"foo%20b%C3%A4\">link</a></p>\n",
    "example": 502,
    "start_line": 7720,
    "end_line": 7724,
    "example": 499,
    "start_line": 7676,
    "end_line": 7680,
    "section": "Links"
  },
  {
    "markdown": "[link](\"title\")\n",
    "html": "<p><a href=\"%22title%22\">link</a></p>\n",
    "example": 503,
    "start_line": 7731,
    "end_line": 7735,
    "example": 500,
    "start_line": 7687,
    "end_line": 7691,
    "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": 504,
    "start_line": 7740,
    "end_line": 7748,
    "example": 501,
    "start_line": 7696,
    "end_line": 7704,
    "section": "Links"
  },
  {
    "markdown": "[link](/url \"title \\\"&quot;\")\n",
    "html": "<p><a href=\"/url\" title=\"title &quot;&quot;\">link</a></p>\n",
    "example": 505,
    "start_line": 7754,
    "end_line": 7758,
    "example": 502,
    "start_line": 7710,
    "end_line": 7714,
    "section": "Links"
  },
  {
    "markdown": "[link](/url \"title\")\n",
    "html": "<p><a href=\"/url%C2%A0%22title%22\">link</a></p>\n",
    "example": 506,
    "start_line": 7765,
    "end_line": 7769,
    "example": 503,
    "start_line": 7720,
    "end_line": 7724,
    "section": "Links"
  },
  {
    "markdown": "[link](/url \"title \"and\" title\")\n",
    "html": "<p>[link](/url &quot;title &quot;and&quot; title&quot;)</p>\n",
    "example": 507,
    "start_line": 7774,
    "end_line": 7778,
    "example": 504,
    "start_line": 7729,
    "end_line": 7733,
    "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": 508,
    "start_line": 7783,
    "end_line": 7787,
    "example": 505,
    "start_line": 7738,
    "end_line": 7742,
    "section": "Links"
  },
  {
    "markdown": "[link](   /uri\n  \"title\"  )\n",
    "html": "<p><a href=\"/uri\" title=\"title\">link</a></p>\n",
    "example": 509,
    "start_line": 7808,
    "end_line": 7813,
    "example": 506,
    "start_line": 7762,
    "end_line": 7767,
    "section": "Links"
  },
  {
    "markdown": "[link] (/uri)\n",
    "html": "<p>[link] (/uri)</p>\n",
    "example": 510,
    "start_line": 7819,
    "end_line": 7823,
    "example": 507,
    "start_line": 7773,
    "end_line": 7777,
    "section": "Links"
  },
  {
    "markdown": "[link [foo [bar]]](/uri)\n",
    "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n",
    "example": 511,
    "start_line": 7829,
    "end_line": 7833,
    "example": 508,
    "start_line": 7783,
    "end_line": 7787,
    "section": "Links"
  },
  {
    "markdown": "[link] bar](/uri)\n",
    "html": "<p>[link] bar](/uri)</p>\n",
    "example": 512,
    "start_line": 7836,
    "end_line": 7840,
    "example": 509,
    "start_line": 7790,
    "end_line": 7794,
    "section": "Links"
  },
  {
    "markdown": "[link [bar](/uri)\n",
    "html": "<p>[link <a href=\"/uri\">bar</a></p>\n",
    "example": 513,
    "start_line": 7843,
    "end_line": 7847,
    "example": 510,
    "start_line": 7797,
    "end_line": 7801,
    "section": "Links"
  },
  {
    "markdown": "[link \\[bar](/uri)\n",
    "html": "<p><a href=\"/uri\">link [bar</a></p>\n",
    "example": 514,
    "start_line": 7850,
    "end_line": 7854,
    "example": 511,
    "start_line": 7804,
    "end_line": 7808,
    "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": 515,
    "start_line": 7859,
    "end_line": 7863,
    "example": 512,
    "start_line": 7813,
    "end_line": 7817,
    "section": "Links"
  },
  {
    "markdown": "[![moon](moon.jpg)](/uri)\n",
    "html": "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\" /></a></p>\n",
    "example": 516,
    "start_line": 7866,
    "end_line": 7870,
    "example": 513,
    "start_line": 7820,
    "end_line": 7824,
    "section": "Links"
  },
  {
    "markdown": "[foo [bar](/uri)](/uri)\n",
    "html": "<p>[foo <a href=\"/uri\">bar</a>](/uri)</p>\n",
    "example": 517,
    "start_line": 7875,
    "end_line": 7879,
    "example": 514,
    "start_line": 7829,
    "end_line": 7833,
    "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": 518,
    "start_line": 7882,
    "end_line": 7886,
    "example": 515,
    "start_line": 7836,
    "end_line": 7840,
    "section": "Links"
  },
  {
    "markdown": "![[[foo](uri1)](uri2)](uri3)\n",
    "html": "<p><img src=\"uri3\" alt=\"[foo](uri2)\" /></p>\n",
    "example": 519,
    "start_line": 7889,
    "end_line": 7893,
    "example": 516,
    "start_line": 7843,
    "end_line": 7847,
    "section": "Links"
  },
  {
    "markdown": "*[foo*](/uri)\n",
    "html": "<p>*<a href=\"/uri\">foo*</a></p>\n",
    "example": 520,
    "start_line": 7899,
    "end_line": 7903,
    "example": 517,
    "start_line": 7853,
    "end_line": 7857,
    "section": "Links"
  },
  {
    "markdown": "[foo *bar](baz*)\n",
    "html": "<p><a href=\"baz*\">foo *bar</a></p>\n",
    "example": 521,
    "start_line": 7906,
    "end_line": 7910,
    "example": 518,
    "start_line": 7860,
    "end_line": 7864,
    "section": "Links"
  },
  {
    "markdown": "*foo [bar* baz]\n",
    "html": "<p><em>foo [bar</em> baz]</p>\n",
    "example": 522,
    "start_line": 7916,
    "end_line": 7920,
    "example": 519,
    "start_line": 7870,
    "end_line": 7874,
    "section": "Links"
  },
  {
    "markdown": "[foo <bar attr=\"](baz)\">\n",
    "html": "<p>[foo <bar attr=\"](baz)\"></p>\n",
    "example": 523,
    "start_line": 7926,
    "end_line": 7930,
    "example": 520,
    "start_line": 7880,
    "end_line": 7884,
    "section": "Links"
  },
  {
    "markdown": "[foo`](/uri)`\n",
    "html": "<p>[foo<code>](/uri)</code></p>\n",
    "example": 524,
    "start_line": 7933,
    "end_line": 7937,
    "example": 521,
    "start_line": 7887,
    "end_line": 7891,
    "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": 525,
    "start_line": 7940,
    "end_line": 7944,
    "example": 522,
    "start_line": 7894,
    "end_line": 7898,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 526,
    "start_line": 7978,
    "end_line": 7984,
    "example": 523,
    "start_line": 7932,
    "end_line": 7938,
    "section": "Links"
  },
  {
    "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n",
    "example": 527,
    "start_line": 7993,
    "end_line": 7999,
    "example": 524,
    "start_line": 7947,
    "end_line": 7953,
    "section": "Links"
  },
  {
    "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n",
    "html": "<p><a href=\"/uri\">link [bar</a></p>\n",
    "example": 528,
    "start_line": 8002,
    "end_line": 8008,
    "example": 525,
    "start_line": 7956,
    "end_line": 7962,
    "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": 529,
    "start_line": 8013,
    "end_line": 8019,
    "example": 526,
    "start_line": 7967,
    "end_line": 7973,
    "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": 530,
    "start_line": 8022,
    "end_line": 8028,
    "example": 527,
    "start_line": 7976,
    "end_line": 7982,
    "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": 531,
    "start_line": 8033,
    "end_line": 8039,
    "example": 528,
    "start_line": 7987,
    "end_line": 7993,
    "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": 532,
    "start_line": 8042,
    "end_line": 8048,
    "example": 529,
    "start_line": 7996,
    "end_line": 8002,
    "section": "Links"
  },
  {
    "markdown": "*[foo*][ref]\n\n[ref]: /uri\n",
    "html": "<p>*<a href=\"/uri\">foo*</a></p>\n",
    "example": 533,
    "start_line": 8057,
    "end_line": 8063,
    "example": 530,
    "start_line": 8011,
    "end_line": 8017,
    "section": "Links"
  },
  {
    "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,
    "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,
    "section": "Links"
  },
  {
    "markdown": "[foo <bar attr=\"][ref]\">\n\n[ref]: /uri\n",
    "html": "<p>[foo <bar attr=\"][ref]\"></p>\n",
    "example": 535,
    "start_line": 8078,
    "end_line": 8084,
    "example": 532,
    "start_line": 8032,
    "end_line": 8038,
    "section": "Links"
  },
  {
    "markdown": "[foo`][ref]`\n\n[ref]: /uri\n",
    "html": "<p>[foo<code>][ref]</code></p>\n",
    "example": 536,
    "start_line": 8087,
    "end_line": 8093,
    "example": 533,
    "start_line": 8041,
    "end_line": 8047,
    "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": 537,
    "start_line": 8096,
    "end_line": 8102,
    "example": 534,
    "start_line": 8050,
    "end_line": 8056,
    "section": "Links"
  },
  {
    "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 538,
    "start_line": 8107,
    "end_line": 8113,
    "example": 535,
    "start_line": 8061,
    "end_line": 8067,
    "section": "Links"
  },
  {
    "markdown": "[ẞ]\n\n[SS]: /url\n",
    "html": "<p><a href=\"/url\"></a></p>\n",
    "example": 539,
    "start_line": 8118,
    "end_line": 8124,
    "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,
    "section": "Links"
  },
  {
    "markdown": "[Foo\n  bar]: /url\n\n[Baz][Foo bar]\n",
    "html": "<p><a href=\"/url\">Baz</a></p>\n",
    "example": 540,
    "start_line": 8130,
    "end_line": 8137,
    "example": 537,
    "start_line": 8084,
    "end_line": 8091,
    "section": "Links"
  },
  {
    "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n",
    "html": "<p>[foo] <a href=\"/url\" title=\"title\">bar</a></p>\n",
    "example": 541,
    "start_line": 8143,
    "end_line": 8149,
    "example": 538,
    "start_line": 8097,
    "end_line": 8103,
    "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": 542,
    "start_line": 8152,
    "end_line": 8160,
    "example": 539,
    "start_line": 8106,
    "end_line": 8114,
    "section": "Links"
  },
  {
    "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n",
    "html": "<p><a href=\"/url1\">bar</a></p>\n",
    "example": 543,
    "start_line": 8193,
    "end_line": 8201,
    "example": 540,
    "start_line": 8147,
    "end_line": 8155,
    "section": "Links"
  },
  {
    "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n",
    "html": "<p>[bar][foo!]</p>\n",
    "example": 544,
    "start_line": 8208,
    "end_line": 8214,
    "example": 541,
    "start_line": 8162,
    "end_line": 8168,
    "section": "Links"
  },
  {
    "markdown": "[foo][ref[]\n\n[ref[]: /uri\n",
    "html": "<p>[foo][ref[]</p>\n<p>[ref[]: /uri</p>\n",
    "example": 545,
    "start_line": 8220,
    "end_line": 8227,
    "example": 542,
    "start_line": 8174,
    "end_line": 8181,
    "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": 546,
    "start_line": 8230,
    "end_line": 8237,
    "example": 543,
    "start_line": 8184,
    "end_line": 8191,
    "section": "Links"
  },
  {
    "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n",
    "html": "<p>[[[foo]]]</p>\n<p>[[[foo]]]: /url</p>\n",
    "example": 547,
    "start_line": 8240,
    "end_line": 8247,
    "example": 544,
    "start_line": 8194,
    "end_line": 8201,
    "section": "Links"
  },
  {
    "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n",
    "html": "<p><a href=\"/uri\">foo</a></p>\n",
    "example": 548,
    "start_line": 8250,
    "end_line": 8256,
    "example": 545,
    "start_line": 8204,
    "end_line": 8210,
    "section": "Links"
  },
  {
    "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n",
    "html": "<p><a href=\"/uri\">bar\\</a></p>\n",
    "example": 549,
    "start_line": 8261,
    "end_line": 8267,
    "example": 546,
    "start_line": 8215,
    "end_line": 8221,
    "section": "Links"
  },
  {
    "markdown": "[]\n\n[]: /uri\n",
    "html": "<p>[]</p>\n<p>[]: /uri</p>\n",
    "example": 550,
    "start_line": 8273,
    "end_line": 8280,
    "example": 547,
    "start_line": 8226,
    "end_line": 8233,
    "section": "Links"
  },
  {
    "markdown": "[\n ]\n\n[\n ]: /uri\n",
    "html": "<p>[\n]</p>\n<p>[\n]: /uri</p>\n",
    "example": 551,
    "start_line": 8283,
    "end_line": 8294,
    "example": 548,
    "start_line": 8236,
    "end_line": 8247,
    "section": "Links"
  },
  {
    "markdown": "[foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 552,
    "start_line": 8306,
    "end_line": 8312,
    "example": 549,
    "start_line": 8259,
    "end_line": 8265,
    "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": 553,
    "start_line": 8315,
    "end_line": 8321,
    "example": 550,
    "start_line": 8268,
    "end_line": 8274,
    "section": "Links"
  },
  {
    "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n",
    "example": 554,
    "start_line": 8326,
    "end_line": 8332,
    "example": 551,
    "start_line": 8279,
    "end_line": 8285,
    "section": "Links"
  },
  {
    "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a>\n[]</p>\n",
    "example": 555,
    "start_line": 8339,
    "end_line": 8347,
    "example": 552,
    "start_line": 8292,
    "end_line": 8300,
    "section": "Links"
  },
  {
    "markdown": "[foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 556,
    "start_line": 8359,
    "end_line": 8365,
    "example": 553,
    "start_line": 8312,
    "end_line": 8318,
    "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": 557,
    "start_line": 8368,
    "end_line": 8374,
    "example": 554,
    "start_line": 8321,
    "end_line": 8327,
    "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": 558,
    "start_line": 8377,
    "end_line": 8383,
    "example": 555,
    "start_line": 8330,
    "end_line": 8336,
    "section": "Links"
  },
  {
    "markdown": "[[bar [foo]\n\n[foo]: /url\n",
    "html": "<p>[[bar <a href=\"/url\">foo</a></p>\n",
    "example": 559,
    "start_line": 8386,
    "end_line": 8392,
    "example": 556,
    "start_line": 8339,
    "end_line": 8345,
    "section": "Links"
  },
  {
    "markdown": "[Foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n",
    "example": 560,
    "start_line": 8397,
    "end_line": 8403,
    "example": 557,
    "start_line": 8350,
    "end_line": 8356,
    "section": "Links"
  },
  {
    "markdown": "[foo] bar\n\n[foo]: /url\n",
    "html": "<p><a href=\"/url\">foo</a> bar</p>\n",
    "example": 561,
    "start_line": 8408,
    "end_line": 8414,
    "example": 558,
    "start_line": 8361,
    "end_line": 8367,
    "section": "Links"
  },
  {
    "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p>[foo]</p>\n",
    "example": 562,
    "start_line": 8420,
    "end_line": 8426,
    "example": 559,
    "start_line": 8373,
    "end_line": 8379,
    "section": "Links"
  },
  {
    "markdown": "[foo*]: /url\n\n*[foo*]\n",
    "html": "<p>*<a href=\"/url\">foo*</a></p>\n",
    "example": 563,
    "start_line": 8432,
    "end_line": 8438,
    "example": 560,
    "start_line": 8385,
    "end_line": 8391,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n",
    "html": "<p><a href=\"/url2\">foo</a></p>\n",
    "example": 564,
    "start_line": 8444,
    "end_line": 8451,
    "example": 561,
    "start_line": 8397,
    "end_line": 8404,
    "section": "Links"
  },
  {
    "markdown": "[foo][]\n\n[foo]: /url1\n",
    "html": "<p><a href=\"/url1\">foo</a></p>\n",
    "example": 565,
    "start_line": 8453,
    "end_line": 8459,
    "example": 562,
    "start_line": 8406,
    "end_line": 8412,
    "section": "Links"
  },
  {
    "markdown": "[foo]()\n\n[foo]: /url1\n",
    "html": "<p><a href=\"\">foo</a></p>\n",
    "example": 566,
    "start_line": 8463,
    "end_line": 8469,
    "example": 563,
    "start_line": 8416,
    "end_line": 8422,
    "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": 567,
    "start_line": 8471,
    "end_line": 8477,
    "example": 564,
    "start_line": 8424,
    "end_line": 8430,
    "section": "Links"
  },
  {
    "markdown": "[foo][bar][baz]\n\n[baz]: /url\n",
    "html": "<p>[foo]<a href=\"/url\">bar</a></p>\n",
    "example": 568,
    "start_line": 8482,
    "end_line": 8488,
    "example": 565,
    "start_line": 8435,
    "end_line": 8441,
    "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": 569,
    "start_line": 8494,
    "end_line": 8501,
    "example": 566,
    "start_line": 8447,
    "end_line": 8454,
    "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": 570,
    "start_line": 8507,
    "end_line": 8514,
    "example": 567,
    "start_line": 8460,
    "end_line": 8467,
    "section": "Links"
  },
  {
    "markdown": "![foo](/url \"title\")\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n",
    "example": 571,
    "start_line": 8530,
    "end_line": 8534,
    "example": 568,
    "start_line": 8483,
    "end_line": 8487,
    "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": 8537,
    "end_line": 8543,
    "example": 569,
    "start_line": 8490,
    "end_line": 8496,
    "section": "Images"
  },
  {
    "markdown": "![foo ![bar](/url)](/url2)\n",
    "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n",
    "example": 573,
    "start_line": 8546,
    "end_line": 8550,
    "example": 570,
    "start_line": 8499,
    "end_line": 8503,
    "section": "Images"
  },
  {
    "markdown": "![foo [bar](/url)](/url2)\n",
    "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n",
    "example": 574,
    "start_line": 8553,
    "end_line": 8557,
    "example": 571,
    "start_line": 8506,
    "end_line": 8510,
    "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": 575,
    "start_line": 8567,
    "end_line": 8573,
    "example": 572,
    "start_line": 8520,
    "end_line": 8526,
    "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": 576,
    "start_line": 8576,
    "end_line": 8582,
    "example": 573,
    "start_line": 8529,
    "end_line": 8535,
    "section": "Images"
  },
  {
    "markdown": "![foo](train.jpg)\n",
    "html": "<p><img src=\"train.jpg\" alt=\"foo\" /></p>\n",
    "example": 577,
    "start_line": 8585,
    "end_line": 8589,
    "example": 574,
    "start_line": 8538,
    "end_line": 8542,
    "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": 578,
    "start_line": 8592,
    "end_line": 8596,
    "example": 575,
    "start_line": 8545,
    "end_line": 8549,
    "section": "Images"
  },
  {
    "markdown": "![foo](<url>)\n",
    "html": "<p><img src=\"url\" alt=\"foo\" /></p>\n",
    "example": 579,
    "start_line": 8599,
    "end_line": 8603,
    "example": 576,
    "start_line": 8552,
    "end_line": 8556,
    "section": "Images"
  },
  {
    "markdown": "![](/url)\n",
    "html": "<p><img src=\"/url\" alt=\"\" /></p>\n",
    "example": 580,
    "start_line": 8606,
    "end_line": 8610,
    "example": 577,
    "start_line": 8559,
    "end_line": 8563,
    "section": "Images"
  },
  {
    "markdown": "![foo][bar]\n\n[bar]: /url\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n",
    "example": 581,
    "start_line": 8615,
    "end_line": 8621,
    "example": 578,
    "start_line": 8568,
    "end_line": 8574,
    "section": "Images"
  },
  {
    "markdown": "![foo][bar]\n\n[BAR]: /url\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n",
    "example": 582,
    "start_line": 8624,
    "end_line": 8630,
    "example": 579,
    "start_line": 8577,
    "end_line": 8583,
    "section": "Images"
  },
  {
    "markdown": "![foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n",
    "example": 583,
    "start_line": 8635,
    "end_line": 8641,
    "example": 580,
    "start_line": 8588,
    "end_line": 8594,
    "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": 584,
    "start_line": 8644,
    "end_line": 8650,
    "example": 581,
    "start_line": 8597,
    "end_line": 8603,
    "section": "Images"
  },
  {
    "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n",
    "example": 585,
    "start_line": 8655,
    "end_line": 8661,
    "example": 582,
    "start_line": 8608,
    "end_line": 8614,
    "section": "Images"
  },
  {
    "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" />\n[]</p>\n",
    "example": 586,
    "start_line": 8667,
    "end_line": 8675,
    "example": 583,
    "start_line": 8620,
    "end_line": 8628,
    "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": 8680,
    "end_line": 8686,
    "example": 584,
    "start_line": 8633,
    "end_line": 8639,
    "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": 588,
    "start_line": 8689,
    "end_line": 8695,
    "example": 585,
    "start_line": 8642,
    "end_line": 8648,
    "section": "Images"
  },
  {
    "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n",
    "html": "<p>![[foo]]</p>\n<p>[[foo]]: /url &quot;title&quot;</p>\n",
    "example": 589,
    "start_line": 8700,
    "end_line": 8707,
    "example": 586,
    "start_line": 8653,
    "end_line": 8660,
    "section": "Images"
  },
  {
    "markdown": "![Foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n",
    "example": 590,
    "start_line": 8712,
    "end_line": 8718,
    "example": 587,
    "start_line": 8665,
    "end_line": 8671,
    "section": "Images"
  },
  {
    "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p>![foo]</p>\n",
    "example": 591,
    "start_line": 8724,
    "end_line": 8730,
    "example": 588,
    "start_line": 8677,
    "end_line": 8683,
    "section": "Images"
  },
  {
    "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n",
    "html": "<p>!<a href=\"/url\" title=\"title\">foo</a></p>\n",
    "example": 592,
    "start_line": 8736,
    "end_line": 8742,
    "example": 589,
    "start_line": 8689,
    "end_line": 8695,
    "section": "Images"
  },
  {
    "markdown": "<http://foo.bar.baz>\n",
    "html": "<p><a href=\"http://foo.bar.baz\">http://foo.bar.baz</a></p>\n",
    "example": 593,
    "start_line": 8769,
    "end_line": 8773,
    "example": 590,
    "start_line": 8722,
    "end_line": 8726,
    "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": 594,
    "start_line": 8776,
    "end_line": 8780,
    "example": 591,
    "start_line": 8729,
    "end_line": 8733,
    "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": 595,
    "start_line": 8783,
    "end_line": 8787,
    "example": 592,
    "start_line": 8736,
    "end_line": 8740,
    "section": "Autolinks"
  },
  {
    "markdown": "<MAILTO:FOO@BAR.BAZ>\n",
    "html": "<p><a href=\"MAILTO:FOO@BAR.BAZ\">MAILTO:FOO@BAR.BAZ</a></p>\n",
    "example": 596,
    "start_line": 8792,
    "end_line": 8796,
    "example": 593,
    "start_line": 8745,
    "end_line": 8749,
    "section": "Autolinks"
  },
  {
    "markdown": "<a+b+c:d>\n",
    "html": "<p><a href=\"a+b+c:d\">a+b+c:d</a></p>\n",
    "example": 597,
    "start_line": 8804,
    "end_line": 8808,
    "example": 594,
    "start_line": 8757,
    "end_line": 8761,
    "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": 598,
    "start_line": 8811,
    "end_line": 8815,
    "example": 595,
    "start_line": 8764,
    "end_line": 8768,
    "section": "Autolinks"
  },
  {
    "markdown": "<http://../>\n",
    "html": "<p><a href=\"http://../\">http://../</a></p>\n",
    "example": 599,
    "start_line": 8818,
    "end_line": 8822,
    "example": 596,
    "start_line": 8771,
    "end_line": 8775,
    "section": "Autolinks"
  },
  {
    "markdown": "<localhost:5001/foo>\n",
    "html": "<p><a href=\"localhost:5001/foo\">localhost:5001/foo</a></p>\n",
    "example": 600,
    "start_line": 8825,
    "end_line": 8829,
    "example": 597,
    "start_line": 8778,
    "end_line": 8782,
    "section": "Autolinks"
  },
  {
    "markdown": "<http://foo.bar/baz bim>\n",
    "html": "<p>&lt;http://foo.bar/baz bim&gt;</p>\n",
    "example": 601,
    "start_line": 8834,
    "end_line": 8838,
    "example": 598,
    "start_line": 8787,
    "end_line": 8791,
    "section": "Autolinks"
  },
  {
    "markdown": "<http://example.com/\\[\\>\n",
    "html": "<p><a href=\"http://example.com/%5C%5B%5C\">http://example.com/\\[\\</a></p>\n",
    "example": 602,
    "start_line": 8843,
    "end_line": 8847,
    "example": 599,
    "start_line": 8796,
    "end_line": 8800,
    "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": 603,
    "start_line": 8865,
    "end_line": 8869,
    "example": 600,
    "start_line": 8818,
    "end_line": 8822,
    "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": 604,
    "start_line": 8872,
    "end_line": 8876,
    "example": 601,
    "start_line": 8825,
    "end_line": 8829,
    "section": "Autolinks"
  },
  {
    "markdown": "<foo\\+@bar.example.com>\n",
    "html": "<p>&lt;foo+@bar.example.com&gt;</p>\n",
    "example": 605,
    "start_line": 8881,
    "end_line": 8885,
    "example": 602,
    "start_line": 8834,
    "end_line": 8838,
    "section": "Autolinks"
  },
  {
    "markdown": "<>\n",
    "html": "<p>&lt;&gt;</p>\n",
    "example": 606,
    "start_line": 8890,
    "end_line": 8894,
    "example": 603,
    "start_line": 8843,
    "end_line": 8847,
    "section": "Autolinks"
  },
  {
    "markdown": "< http://foo.bar >\n",
    "html": "<p>&lt; http://foo.bar &gt;</p>\n",
    "example": 607,
    "start_line": 8897,
    "end_line": 8901,
    "example": 604,
    "start_line": 8850,
    "end_line": 8854,
    "section": "Autolinks"
  },
  {
    "markdown": "<m:abc>\n",
    "html": "<p>&lt;m:abc&gt;</p>\n",
    "example": 608,
    "start_line": 8904,
    "end_line": 8908,
    "example": 605,
    "start_line": 8857,
    "end_line": 8861,
    "section": "Autolinks"
  },
  {
    "markdown": "<foo.bar.baz>\n",
    "html": "<p>&lt;foo.bar.baz&gt;</p>\n",
    "example": 609,
    "start_line": 8911,
    "end_line": 8915,
    "example": 606,
    "start_line": 8864,
    "end_line": 8868,
    "section": "Autolinks"
  },
  {
    "markdown": "http://example.com\n",
    "html": "<p>http://example.com</p>\n",
    "example": 610,
    "start_line": 8918,
    "end_line": 8922,
    "example": 607,
    "start_line": 8871,
    "end_line": 8875,
    "section": "Autolinks"
  },
  {
    "markdown": "foo@bar.example.com\n",
    "html": "<p>foo@bar.example.com</p>\n",
    "example": 611,
    "start_line": 8925,
    "end_line": 8929,
    "example": 608,
    "start_line": 8878,
    "end_line": 8882,
    "section": "Autolinks"
  },
  {
    "markdown": "<a><bab><c2c>\n",
    "html": "<p><a><bab><c2c></p>\n",
    "example": 612,
    "start_line": 9006,
    "end_line": 9010,
    "example": 609,
    "start_line": 8960,
    "end_line": 8964,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a/><b2/>\n",
    "html": "<p><a/><b2/></p>\n",
    "example": 613,
    "start_line": 9015,
    "end_line": 9019,
    "example": 610,
    "start_line": 8969,
    "end_line": 8973,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a  /><b2\ndata=\"foo\" >\n",
    "html": "<p><a  /><b2\ndata=\"foo\" ></p>\n",
    "example": 614,
    "start_line": 9024,
    "end_line": 9030,
    "example": 611,
    "start_line": 8978,
    "end_line": 8984,
    "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": 615,
    "start_line": 9035,
    "end_line": 9041,
    "example": 612,
    "start_line": 8989,
    "end_line": 8995,
    "section": "Raw HTML"
  },
  {
    "markdown": "Foo <responsive-image src=\"foo.jpg\" />\n",
    "html": "<p>Foo <responsive-image src=\"foo.jpg\" /></p>\n",
    "example": 616,
    "start_line": 9046,
    "end_line": 9050,
    "example": 613,
    "start_line": 9000,
    "end_line": 9004,
    "section": "Raw HTML"
  },
  {
    "markdown": "<33> <__>\n",
    "html": "<p>&lt;33&gt; &lt;__&gt;</p>\n",
    "example": 617,
    "start_line": 9055,
    "end_line": 9059,
    "example": 614,
    "start_line": 9009,
    "end_line": 9013,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a h*#ref=\"hi\">\n",
    "html": "<p>&lt;a h*#ref=&quot;hi&quot;&gt;</p>\n",
    "example": 618,
    "start_line": 9064,
    "end_line": 9068,
    "example": 615,
    "start_line": 9018,
    "end_line": 9022,
    "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": 619,
    "start_line": 9073,
    "end_line": 9077,
    "example": 616,
    "start_line": 9027,
    "end_line": 9031,
    "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": 620,
    "start_line": 9082,
    "end_line": 9092,
    "example": 617,
    "start_line": 9036,
    "end_line": 9046,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a href='bar'title=title>\n",
    "html": "<p>&lt;a href='bar'title=title&gt;</p>\n",
    "example": 621,
    "start_line": 9097,
    "end_line": 9101,
    "example": 618,
    "start_line": 9051,
    "end_line": 9055,
    "section": "Raw HTML"
  },
  {
    "markdown": "</a></foo >\n",
    "html": "<p></a></foo ></p>\n",
    "example": 622,
    "start_line": 9106,
    "end_line": 9110,
    "example": 619,
    "start_line": 9060,
    "end_line": 9064,
    "section": "Raw HTML"
  },
  {
    "markdown": "</a href=\"foo\">\n",
    "html": "<p>&lt;/a href=&quot;foo&quot;&gt;</p>\n",
    "example": 623,
    "start_line": 9115,
    "end_line": 9119,
    "example": 620,
    "start_line": 9069,
    "end_line": 9073,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <!-- this is a\ncomment - with hyphen -->\n",
    "html": "<p>foo <!-- this is a\ncomment - with hyphen --></p>\n",
    "example": 624,
    "start_line": 9124,
    "end_line": 9130,
    "example": 621,
    "start_line": 9078,
    "end_line": 9084,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <!-- not a comment -- two hyphens -->\n",
    "html": "<p>foo &lt;!-- not a comment -- two hyphens --&gt;</p>\n",
    "example": 625,
    "start_line": 9133,
    "end_line": 9137,
    "example": 622,
    "start_line": 9087,
    "end_line": 9091,
    "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": 626,
    "start_line": 9142,
    "end_line": 9149,
    "example": 623,
    "start_line": 9096,
    "end_line": 9103,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <?php echo $a; ?>\n",
    "html": "<p>foo <?php echo $a; ?></p>\n",
    "example": 627,
    "start_line": 9154,
    "end_line": 9158,
    "example": 624,
    "start_line": 9108,
    "end_line": 9112,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <!ELEMENT br EMPTY>\n",
    "html": "<p>foo <!ELEMENT br EMPTY></p>\n",
    "example": 628,
    "start_line": 9163,
    "end_line": 9167,
    "example": 625,
    "start_line": 9117,
    "end_line": 9121,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <![CDATA[>&<]]>\n",
    "html": "<p>foo <![CDATA[>&<]]></p>\n",
    "example": 629,
    "start_line": 9172,
    "end_line": 9176,
    "example": 626,
    "start_line": 9126,
    "end_line": 9130,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <a href=\"&ouml;\">\n",
    "html": "<p>foo <a href=\"&ouml;\"></p>\n",
    "example": 630,
    "start_line": 9182,
    "end_line": 9186,
    "example": 627,
    "start_line": 9136,
    "end_line": 9140,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo <a href=\"\\*\">\n",
    "html": "<p>foo <a href=\"\\*\"></p>\n",
    "example": 631,
    "start_line": 9191,
    "end_line": 9195,
    "example": 628,
    "start_line": 9145,
    "end_line": 9149,
    "section": "Raw HTML"
  },
  {
    "markdown": "<a href=\"\\\"\">\n",
    "html": "<p>&lt;a href=&quot;&quot;&quot;&gt;</p>\n",
    "example": 632,
    "start_line": 9198,
    "end_line": 9202,
    "example": 629,
    "start_line": 9152,
    "end_line": 9156,
    "section": "Raw HTML"
  },
  {
    "markdown": "foo  \nbaz\n",
    "html": "<p>foo<br />\nbaz</p>\n",
    "example": 633,
    "start_line": 9212,
    "end_line": 9218,
    "example": 630,
    "start_line": 9166,
    "end_line": 9172,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\\\nbaz\n",
    "html": "<p>foo<br />\nbaz</p>\n",
    "example": 634,
    "start_line": 9224,
    "end_line": 9230,
    "example": 631,
    "start_line": 9178,
    "end_line": 9184,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo       \nbaz\n",
    "html": "<p>foo<br />\nbaz</p>\n",
    "example": 635,
    "start_line": 9235,
    "end_line": 9241,
    "example": 632,
    "start_line": 9189,
    "end_line": 9195,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo  \n     bar\n",
    "html": "<p>foo<br />\nbar</p>\n",
    "example": 636,
    "start_line": 9246,
    "end_line": 9252,
    "example": 633,
    "start_line": 9200,
    "end_line": 9206,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\\\n     bar\n",
    "html": "<p>foo<br />\nbar</p>\n",
    "example": 637,
    "start_line": 9255,
    "end_line": 9261,
    "example": 634,
    "start_line": 9209,
    "end_line": 9215,
    "section": "Hard line breaks"
  },
  {
    "markdown": "*foo  \nbar*\n",
    "html": "<p><em>foo<br />\nbar</em></p>\n",
    "example": 638,
    "start_line": 9267,
    "end_line": 9273,
    "example": 635,
    "start_line": 9221,
    "end_line": 9227,
    "section": "Hard line breaks"
  },
  {
    "markdown": "*foo\\\nbar*\n",
    "html": "<p><em>foo<br />\nbar</em></p>\n",
    "example": 639,
    "start_line": 9276,
    "end_line": 9282,
    "example": 636,
    "start_line": 9230,
    "end_line": 9236,
    "section": "Hard line breaks"
  },
  {
    "markdown": "`code  \nspan`\n",
    "html": "<p><code>code   span</code></p>\n",
    "example": 640,
    "start_line": 9287,
    "end_line": 9292,
    "markdown": "`code \nspan`\n",
    "html": "<p><code>code  span</code></p>\n",
    "example": 637,
    "start_line": 9241,
    "end_line": 9246,
    "section": "Hard line breaks"
  },
  {
    "markdown": "`code\\\nspan`\n",
    "html": "<p><code>code\\ span</code></p>\n",
    "example": 641,
    "start_line": 9295,
    "end_line": 9300,
    "example": 638,
    "start_line": 9249,
    "end_line": 9254,
    "section": "Hard line breaks"
  },
  {
    "markdown": "<a href=\"foo  \nbar\">\n",
    "html": "<p><a href=\"foo  \nbar\"></p>\n",
    "example": 642,
    "start_line": 9305,
    "end_line": 9311,
    "example": 639,
    "start_line": 9259,
    "end_line": 9265,
    "section": "Hard line breaks"
  },
  {
    "markdown": "<a href=\"foo\\\nbar\">\n",
    "html": "<p><a href=\"foo\\\nbar\"></p>\n",
    "example": 643,
    "start_line": 9314,
    "end_line": 9320,
    "example": 640,
    "start_line": 9268,
    "end_line": 9274,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\\\n",
    "html": "<p>foo\\</p>\n",
    "example": 644,
    "start_line": 9327,
    "end_line": 9331,
    "example": 641,
    "start_line": 9281,
    "end_line": 9285,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo  \n",
    "html": "<p>foo</p>\n",
    "example": 645,
    "start_line": 9334,
    "end_line": 9338,
    "example": 642,
    "start_line": 9288,
    "end_line": 9292,
    "section": "Hard line breaks"
  },
  {
    "markdown": "### foo\\\n",
    "html": "<h3>foo\\</h3>\n",
    "example": 646,
    "start_line": 9341,
    "end_line": 9345,
    "example": 643,
    "start_line": 9295,
    "end_line": 9299,
    "section": "Hard line breaks"
  },
  {
    "markdown": "### foo  \n",
    "html": "<h3>foo</h3>\n",
    "example": 647,
    "start_line": 9348,
    "end_line": 9352,
    "example": 644,
    "start_line": 9302,
    "end_line": 9306,
    "section": "Hard line breaks"
  },
  {
    "markdown": "foo\nbaz\n",
    "html": "<p>foo\nbaz</p>\n",
    "example": 648,
    "start_line": 9363,
    "end_line": 9369,
    "example": 645,
    "start_line": 9317,
    "end_line": 9323,
    "section": "Soft line breaks"
  },
  {
    "markdown": "foo \n baz\n",
    "html": "<p>foo\nbaz</p>\n",
    "example": 649,
    "start_line": 9375,
    "end_line": 9381,
    "example": 646,
    "start_line": 9329,
    "end_line": 9335,
    "section": "Soft line breaks"
  },
  {
    "markdown": "hello $.;'there\n",
    "html": "<p>hello $.;'there</p>\n",
    "example": 650,
    "start_line": 9395,
    "end_line": 9399,
    "example": 647,
    "start_line": 9349,
    "end_line": 9353,
    "section": "Textual content"
  },
  {
    "markdown": "Foo χρῆν\n",
    "html": "<p>Foo χρῆν</p>\n",
    "example": 651,
    "start_line": 9402,
    "end_line": 9406,
    "example": 648,
    "start_line": 9356,
    "end_line": 9360,
    "section": "Textual content"
  },
  {
    "markdown": "Multiple     spaces\n",
    "html": "<p>Multiple     spaces</p>\n",
    "example": 652,
    "start_line": 9411,
    "end_line": 9415,
    "example": 649,
    "start_line": 9365,
    "end_line": 9369,
    "section": "Textual content"
  }
]

Deleted testdata/testbox/00000000000100.zettel.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
expert-mode: true
modified: 20210629174242
no-index: true
visibility: owner

Deleted testdata/testbox/19700101000000.zettel.

1
2
3
4
5
6
7
8
9
10
11











-
-
-
-
-
-
-
-
-
-
-
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
token-lifetime-api: 1
visibility: owner

Deleted testdata/testbox/20210629163300.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
id: 20210629163300
title: owner
role: user
tags: #test #user
syntax: none
credential: $2a$10$gcKyVmQ50fwgpOjyiiCm4eba/ILrNXoxTUCopgTEnYTa4yuceHMC6
modified: 20210629173617
user-id: owner

Deleted testdata/testbox/20210629165000.zettel.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
id: 20210629165000
title: writer
role: user
tags: #test #user
syntax: none
credential: $2a$10$VmHPyXa0Bm8DE4MJ.pQnbuuQmweWtyGya0L/bFA4nIuCn1EvPQflK
modified: 20210629173536
user-id: writer
user-role: writer

Deleted testdata/testbox/20210629165024.zettel.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
id: 20210629165024
title: reader
role: user
tags: #test #user
syntax: none
credential: $2a$10$uC7LV2JdFhasw2HqSWZbSOihvFpwtaEXjXp98yzGfE3FHudq.vg.u
modified: 20210629173459
user-id: reader
user-role: reader

Deleted testdata/testbox/20210629165050.zettel.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
id: 20210629165050
title: creator
role: user
tags: #test #user
syntax: none
credential: $2a$10$z85253tqhbHlXPZpt0hJpughLR4WXY8iYJbm1LlBhrKsL1YfkRy2q
modified: 20210629173424
user-id: creator
user-role: creator

Changes to tests/markdown_test.go.

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

22
23
24
25
26
27
28







-







	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strings"
	"testing"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"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"
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66




















67
68

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
40
41
42
43
44
45
46



















47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66


67
68
69
70
71
72

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

87
88
89
90
91
92
93







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





-














-







	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", // 170
	"<script>\nfoo\n</script>1. *bar*\n",                       // 178
	"- foo\n  - bar\n    - baz\n      - boo\n",                 // 294
	"10) foo\n    - bar\n",                                     // 296
	"- # Foo\n- Bar\n  ---\n  baz\n",                           // 300
	"- foo\n\n- bar\n\n\n- baz\n",                              // 306
	"- foo\n  - bar\n    - baz\n\n\n      bim\n",               // 307
	"1. a\n\n  2. b\n\n   3. c\n",                              // 311
	"1. a\n\n  2. b\n\n    3. c\n",                             // 313
	"- a\n- b\n\n- c\n",                                        // 314
	"* a\n*\n\n* c\n",                                          // 315
	"- a\n- b\n\n  [ref]: /url\n- d\n",                         // 317
	"- a\n  - b\n\n    c\n- d\n",                               // 319
	"* a\n  > b\n  >\n* c\n",                                   // 320
	"- a\n  > b\n  ```\n  c\n  ```\n- d\n",                     // 321
	"- a\n  - b\n",                                             // 323
	"<http://foo.bar.`baz>`\n",                                 // 345
	"[foo<http://example.com/?search=](uri)>\n",                // 525
	"[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 537
	"<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://example.com?find=\\*>\n",                          // 581
	"<http://foo.bar.baz/test?q=hello&id=22&boolean>\n",        // 594
	"<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)
		if enc == nil {
			t.Errorf("No encoder for %q found", format)
			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)
117
118
119
120
121
122
123
124

125
126
127
128
129
130
131
114
115
116
117
118
119
120

121
122
123
124
125
126
127
128







-
+







			encoder.Create(format, nil).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) {
	htmlEncoder := encoder.Create(api.EncoderHTML, &encoder.Environment{Xhtml: true})
	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()

142
143
144
145
146
147
148
149

150
151
152
153
154
155
156
139
140
141
142
143
144
145

146
147
148
149
150
151
152
153







-
+







				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(api.EncoderZmk, nil)
	zmkEncoder := encoder.Create("zmk", nil)
	var sb strings.Builder
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		zmkEncoder.WriteBlocks(&sb, ast)
		gotFirst := sb.String()
		sb.Reset()

Changes to tests/regression_test.go.

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


34
35
36
37
38
39
40
41
42

43
44
45

46
47
48
49
50

51
52

53
54
55
56
57
58
59
60
61
62

63
64
65
66

67
68
69
70

71
72
73

74
75
76
77
78
79


80
81
82
83
84
85
86
17
18
19
20
21
22
23

24


25
26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
42
43

44





45


46
47
48
49
50
51
52
53
54
55

56
57
58
59

60
61
62
63

64
65
66

67
68
69
70
71


72
73
74
75
76
77
78
79
80







-

-
-






+
+

-







+


-
+
-
-
-
-
-
+
-
-
+









-
+



-
+



-
+


-
+




-
-
+
+







	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/manager"

	_ "zettelstore.de/z/box/dirbox"
	_ "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/parser/zettelmark"
	_ "zettelstore.de/z/place/dirplace"
)

var formats = []api.EncodingEnum{
var formats = []string{"html", "djson", "native", "text"}
	api.EncoderHTML,
	api.EncoderDJSON,
	api.EncoderNative,
	api.EncoderText,
}


func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) {
func getFilePlaces(wd string, kind string) (root string, places []place.ManagedPlace) {
	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}
	for _, entry := range entries {
		if entry.IsDir() {
			u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple)
			u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.PlaceDirTypeSimple)
			if err != nil {
				panic(err)
			}
			box, err := manager.Connect(u, &noAuth{}, &cdata)
			place, err := manager.Connect(u, &noAuth{}, &cdata)
			if err != nil {
				panic(err)
			}
			boxes = append(boxes, box)
			places = append(places, place)
		}
	}
	return root, boxes
	return root, places
}

type noEnrich struct{}

func (nf *noEnrich) Enrich(context.Context, *meta.Meta, int) {}
func (nf *noEnrich) Remove(context.Context, *meta.Meta)      {}
func (nf *noEnrich) Enrich(ctx context.Context, m *meta.Meta) {}
func (nf *noEnrich) Remove(ctx context.Context, m *meta.Meta) {}

type noAuth struct{}

func (na *noAuth) IsReadonly() bool { return false }

func trimLastEOL(s string) string {
	if lastPos := len(s) - 1; lastPos >= 0 && s[lastPos] == '\n' {
95
96
97
98
99
100
101
102

103
104
105
106
107
108
109
110
111
112
113
114
115
116

117
118
119
120
121
122
123
124
125
126
127
128
129

130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

147
148
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
89
90
91
92
93
94
95

96
97
98
99
100
101
102
103
104
105
106
107
108
109

110
111
112
113
114
115
116
117
118
119
120
121
122

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139

140
141
142
143
144
145
146
147
148
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







-
+













-
+












-
+
















-
+









-
-
+
+















-
+













-




-
-
-
+
+
+



-
+











-
-
+
+















-
+







		return "", err
	}
	defer f.Close()
	src, err := io.ReadAll(f)
	return string(src), err
}

func checkFileContent(t *testing.T, filename, gotContent string) {
func checkFileContent(t *testing.T, filename string, 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 api.EncodingEnum) {
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(api.EncoderZmk, nil)
	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 getBoxName(p box.ManagedBox, root string) string {
func getPlaceName(p place.ManagedPlace, 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 match(*meta.Meta) bool { return true }

func checkContentBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)
func checkContentPlace(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", "content", boxName, z.Zid.String()+"."+format.String())
				resultName := filepath.Join(wd, "result", "content", placeName, z.Zid.String()+"."+format)
				checkBlocksFile(st, resultName, z, format)
			})
		}
		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 {
		panic(err)
	}
}

func TestContentRegression(t *testing.T) {
	t.Parallel()
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, boxes := getFileBoxes(wd, "content")
	for _, p := range boxes {
		checkContentBox(t, p, wd, getBoxName(p, root))
	root, places := getFilePlaces(wd, "content")
	for _, p := range places {
		checkContentPlace(t, p, wd, getPlaceName(p, root))
	}
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format api.EncodingEnum) {
func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) {
	t.Helper()

	if enc := encoder.Create(format, nil); enc != nil {
		var sb strings.Builder
		enc.WriteMeta(&sb, zn.Meta)
		checkFileContent(t, resultName, sb.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer format %q", format))
}

func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)
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", boxName, z.Zid.String()+"."+format.String())
				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)
	}
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266



267
268
245
246
247
248
249
250
251

252
253
254
255



256
257
258
259
260







-




-
-
-
+
+
+



func (cfg *myConfig) GetExpertMode() bool                      { return false }
func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() }

var testConfig = &myConfig{}

func TestMetaRegression(t *testing.T) {
	t.Parallel()
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, boxes := getFileBoxes(wd, "meta")
	for _, p := range boxes {
		checkMetaBox(t, p, wd, getBoxName(p, root))
	root, places := getFilePlaces(wd, "meta")
	for _, p := range places {
		checkMetaPlace(t, p, wd, getPlaceName(p, root))
	}
}

Changes to tools/build.go.

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

61











62
63
64
65
66
67
68
10
11
12
13
14
15
16

17
18
19
20

21
22
23
24
25
26
27
28
29
30
31




















32
33
34
35
36
37

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







-




-











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






-
+

+
+
+
+
+
+
+
+
+
+
+








// Package main provides a command to build and run the software.
package main

import (
	"archive/zip"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"zettelstore.de/z/strfun"
)

func executeCommand(env []string, name string, arg ...string) (string, error) {
	logCommand("EXEC", env, name, arg)
	var out bytes.Buffer
	cmd := prepareCommand(env, name, arg, &out)
	err := cmd.Run()
	return out.String(), err
}

func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd {
	if len(env) > 0 {
		env = append(env, os.Environ()...)
	}
	cmd := exec.Command(name, arg...)
	cmd.Env = env
	cmd.Stdin = nil
	cmd.Stdout = out
	cmd.Stderr = os.Stderr
	return cmd
}

func logCommand(exec string, env []string, name string, arg []string) {
	if verbose {
		if len(env) > 0 {
			for i, e := range env {
				fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e)
			}
		}
		fmt.Fprintln(os.Stderr, exec, name, arg)
		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
	}
131
132
133
134
135
136
137
138

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

157
158
159

160
161
162
163
164
165
166
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144

145



146
147
148
149
150
151
152
153







-
+

















-
+
-
-
-
+







	if path, err := executeCommand(nil, "which", "shadow"); err == nil && path != "" {
		return path
	}
	return ""
}

func cmdCheck() error {
	if err := checkGoTest("./..."); err != nil {
	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(pkg string, testParams ...string) error {
func checkGoTest() error {
	args := []string{"test", pkg}
	args = append(args, testParams...)
	out, err := executeCommand(nil, "go", args...)
	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)
		}
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
215
216
217
218
219
220
221




































































222
223
224
225
226
227
228







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







			fmt.Fprintf(os.Stderr, " %q", extra)
		}
		fmt.Fprintln(os.Stderr)
	}
	return nil
}

type zsInfo struct {
	cmd          *exec.Cmd
	out          bytes.Buffer
	adminAddress string
}

func cmdTestAPI() error {
	var err error
	var info zsInfo
	needServer := !addressInUse(":23123")
	if needServer {
		err = startZettelstore(&info)
	}
	if err != nil {
		return err
	}
	err = checkGoTest("zettelstore.de/z/client", "-base-url", "http://127.0.0.1:23123")
	if needServer {
		err1 := stopZettelstore(&info)
		if err == nil {
			err = err1
		}
	}
	return err
}

func startZettelstore(info *zsInfo) error {
	info.adminAddress = ":2323"
	name, arg := "go", []string{
		"run", "cmd/zettelstore/main.go", "run",
		"-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
	logCommand("FORK", nil, name, arg)
	cmd := prepareCommand(nil, name, arg, &info.out)
	if !verbose {
		cmd.Stderr = nil
	}
	err := cmd.Start()
	for i := 0; i < 100; i++ {
		time.Sleep(time.Millisecond * 100)
		if addressInUse(info.adminAddress) {
			info.cmd = cmd
			return err
		}
	}
	return errors.New("zettelstore did not start")
}

func stopZettelstore(i *zsInfo) error {
	conn, err := net.Dial("tcp", i.adminAddress)
	if err != nil {
		fmt.Println("Unable to stop Zettelstore")
		return err
	}
	io.WriteString(conn, "shutdown\n")
	conn.Close()
	err = i.cmd.Wait()
	return err
}

func addressInUse(address string) bool {
	conn, err := net.Dial("tcp", address)
	if err != nil {
		return false
	}
	conn.Close()
	return true
}

func cmdBuild() error {
	return doBuild(nil, getVersion(), "bin/zettelstore")
}

func doBuild(env []string, version, target string) error {
	out, err := executeCommand(
		env,
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
406
407
408
409
410
411
412

413
414
415
416
417
418
419







-







           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.
  testapi  Starts a Zettelstore and execute API tests.
  version  Print the current version of the software.

All commands can be abbreviated as long as they remain unique.`)
}

var (
	verbose bool
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
436
437
438
439
440
441
442


443
444
445
446
447
448
449
450
451
452
453
454







-
-












			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 "t", "te", "tes", "test", "testa", "testap", "testapi":
			cmdTestAPI()
		case "h", "he", "hel", "help":
			cmdHelp()
		default:
			fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0])
			cmdHelp()
			os.Exit(1)
		}
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

Changes to usecase/context.go.

40
41
42
43
44
45
46











47
48
49
50
51
52
53
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64







+
+
+
+
+
+
+
+
+
+
+







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

Changes to usecase/copy_zettel.go.

10
11
12
13
14
15
16

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







+








// 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 {
31
32
33
34
35
36
37
38

39
40

41
32
33
34
35
36
37
38

39


40
41







-
+
-
-
+

		if len(title) > 0 {
			title = "Copy of " + title
		} else {
			title = "Copy"
		}
		m.Set(meta.KeyTitle, title)
	}
	content := origZettel.Content
	content := strfun.TrimSpaceRight(origZettel.Content.AsString())
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
	return domain.Zettel{Meta: m, Content: domain.Content(content)}
}

Changes to usecase/create_zettel.go.

14
15
16
17
18
19
20

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







+







import (
	"context"

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

// 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)
}
54
55
56
57
58
59
60
61

62
63
55
56
57
58
59
60
61

62
63
64







-
+


		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.TrimSpace()
	zettel.Content = domain.Content(strfun.TrimSpaceRight(zettel.Content.AsString()))
	return uc.port.CreateZettel(ctx, zettel)
}

Changes to usecase/delete_zettel.go.

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29







-
+







	"context"

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

// DeleteZettelPort is the interface used by this use case.
type DeleteZettelPort interface {
	// DeleteZettel removes the zettel from the box.
	// DeleteZettel removes the zettel from the place.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

// DeleteZettel is the data for this use case.
type DeleteZettel struct {
	port DeleteZettelPort
}

Changes to usecase/folge_zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
40
41
42
43
44
45
46
47

48
40
41
42
43
44
45
46

47
48







-
+

		}
		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: domain.NewContent("")}
	return domain.Zettel{Meta: m, Content: ""}
}

Deleted usecase/get_all_meta.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

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

// GetAllMetaPort is the interface used by this use case.
type GetAllMetaPort interface {
	// GetAllMeta retrieves just the meta data of a specific zettel.
	GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error)
}

// GetAllMeta is the data for this use case.
type GetAllMeta struct {
	port GetAllMetaPort
}

// NewGetAllMeta creates a new use case.
func NewGetAllMeta(port GetAllMetaPort) GetAllMeta {
	return GetAllMeta{port: port}
}

// Run executes the use case.
func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
	return uc.port.GetAllMeta(ctx, zid)
}

Changes to usecase/get_user.go.

11
12
13
14
15
16
17
18
19
20



21
22
23
24
25
26
27
11
12
13
14
15
16
17



18
19
20
21
22
23
24
25
26
27







-
-
-
+
+
+







// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/search"
)

// Use case: return user identified by meta key ident.
// ---------------------------------------------------

// GetUserPort is the interface used by this use case.
39
40
41
42
43
44
45
46

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

46
47
48
49
50
51
52
53







-
+







// NewGetUser creates a new use case.
func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser {
	return GetUser{authz: authz, port: port}
}

// Run executes the use case.
func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) {
	ctx = box.NoEnrichContext(ctx)
	ctx = place.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 {
		if role, ok := identMeta.Get(meta.KeyRole); !ok ||
86
87
88
89
90
91
92
93

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

93
94
95
96
97
98
99
100
101
102







-
+









// NewGetUserByZid creates a new use case.
func NewGetUserByZid(port GetUserByZidPort) GetUserByZid {
	return GetUserByZid{port: port}
}

// GetUser executes the use case.
func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) {
	userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid)
	userMeta, err := uc.port.GetMeta(place.NoEnrichContext(ctx), zid)
	if err != nil {
		return nil, err
	}

	if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident {
		return nil, nil
	}
	return userMeta, nil
}

Changes to usecase/list_role.go.

11
12
13
14
15
16
17
18
19


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


18
19
20
21
22
23
24
25
26







-
-
+
+







// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"
	"sort"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/meta"
	"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)
34
35
36
37
38
39
40
41

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

41
42
43
44
45
46
47
48







-
+







// 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(box.NoEnrichContext(ctx), nil)
	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

Changes to usecase/list_tags.go.

10
11
12
13
14
15
16
17
18


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


17
18
19
20
21
22
23
24
25







-
-
+
+








// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/meta"
	"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)
36
37
38
39
40
41
42
43

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

43
44
45
46
47
48
49
50







-
+







}

// 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(box.NoEnrichContext(ctx), nil)
	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 {

Changes to usecase/new_zettel.go.

9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
39
40





41
42
43

44
45

46
9
10
11
12
13
14
15

16

17
18
19
20
21
22
23
24
25
26
27
28


29









30
31
32
33
34
35
36

37


38
39







-
+
-












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


-
+
-
-
+

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

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/domain/meta"
)

// 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 := meta.New(id.Invalid)
	om := origZettel.Meta
	m := origZettel.Meta.Clone()
	m.Set(meta.KeyTitle, om.GetDefault(meta.KeyTitle, ""))
	m.Set(meta.KeyRole, om.GetDefault(meta.KeyRole, ""))
	m.Set(meta.KeyTags, om.GetDefault(meta.KeyTags, ""))
	m.Set(meta.KeySyntax, om.GetDefault(meta.KeySyntax, ""))

	const prefixLen = len(meta.NewPrefix)
	for _, pair := range om.PairsRest(false) {
		if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
			m.Set(key[prefixLen:], pair.Value)
	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 := origZettel.Content
	content := strfun.TrimSpaceRight(origZettel.Content.AsString())
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
	return domain.Zettel{Meta: m, Content: domain.Content(content)}
}

Changes to usecase/order.go.

33
34
35
36
37
38
39
40
41



42
43
44
45
46
47
48
49
50
51
52
53
54
55
33
34
35
36
37
38
39


40
41
42

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







-
-
+
+
+
-














// 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,
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/rename_zettel.go.

10
11
12
13
14
15
16
17
18
19



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

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

51
52
53
54
55
56
57
58
59
60
61
62
10
11
12
13
14
15
16



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

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

50
51
52
53
54
55
56
57
58
59
60
61
62







-
-
-
+
+
+
















-
+













-
+













// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

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

// RenameZettelPort is the interface used by this use case.
type RenameZettelPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)

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

// RenameZettel is the data for this use case.
type RenameZettel struct {
	port RenameZettelPort
}

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

func (err *ErrZidInUse) Error() string {
	return "Zettel id already in use: " + err.Zid.String()
}

// NewRenameZettel creates a new use case.
func NewRenameZettel(port RenameZettelPort) RenameZettel {
	return RenameZettel{port: port}
}

// Run executes the use case.
func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
	noEnrichCtx := box.NoEnrichContext(ctx)
	noEnrichCtx := place.NoEnrichContext(ctx)
	if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil {
		return err
	}
	if newZid == curZid {
		// Nothing to do
		return nil
	}
	if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil {
		return &ErrZidInUse{Zid: newZid}
	}
	return uc.port.RenameZettel(ctx, curZid, newZid)
}

Changes to usecase/search.go.

10
11
12
13
14
15
16
17
18


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


17
18
19
20
21
22
23
24
25







-
-
+
+








// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/meta"
	"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)
34
35
36
37
38
39
40
41

42
43
44
34
35
36
37
38
39
40

41
42
43
44







-
+



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 = box.NoEnrichContext(ctx)
		ctx = place.NoEnrichContext(ctx)
	}
	return uc.port.SelectMeta(ctx, s)
}

Changes to usecase/update_zettel.go.

10
11
12
13
14
15
16
17
18
19
20


21
22
23
24
25
26
27
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27
28







-



+
+








// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/strfun"
)

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

38
39
40
41
42
43
44
45

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

60
61
62
39
40
41
42
43
44
45

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


59
60
61
62







-
+












-
-
+



func NewUpdateZettel(port UpdateZettelPort) UpdateZettel {
	return UpdateZettel{port: port}
}

// Run executes the use case.
func (uc UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error {
	m := zettel.Meta
	oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid)
	oldZettel, err := uc.port.GetZettel(place.NoEnrichContext(ctx), m.Zid)
	if err != nil {
		return err
	}
	if zettel.Equal(oldZettel, false) {
		return nil
	}
	m.SetNow(meta.KeyModified)
	m.YamlSep = oldZettel.Meta.YamlSep
	if m.Zid == id.ConfigurationZid {
		m.Set(meta.KeySyntax, meta.ValueSyntaxNone)
	}
	if !hasContent {
		zettel.Content = oldZettel.Content
		zettel.Content.TrimSpace()
		zettel.Content = domain.Content(strfun.TrimSpaceRight(oldZettel.Content.AsString()))
	}
	return uc.port.UpdateZettel(ctx, zettel)
}

Changes to web/adapter/api/api.go.

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

18
19
20
21
22
23
24







-







// Package api provides api handlers for web requests.
package api

import (
	"context"
	"time"

	"zettelstore.de/z/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/web/server"
)

48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
47
48
49
50
51
52
53

54
55
56
57
58
59
60
61
62







-
+








	return api
}

// 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) *api.URLBuilder { return api.b.NewURLBuilder(key) }
func (api *API) NewURLBuilder(key byte) server.URLBuilder { return api.b.NewURLBuilder(key) }

func (api *API) getAuthData(ctx context.Context) *server.AuthData {
	return api.auth.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)
}

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

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


14
15








16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33

-
+











-
-


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


-
+







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

import "zettelstore.de/z/api"

const plainText = "text/plain; charset=utf-8"

var mapFormat2CT = map[api.EncodingEnum]string{
	api.EncoderHTML:   "text/html; charset=utf-8",
	api.EncoderNative: plainText,
	api.EncoderJSON:   "application/json",
	api.EncoderDJSON:  "application/json",
	api.EncoderText:   plainText,
	api.EncoderZmk:    plainText,
	api.EncoderRaw:    plainText, // In some cases...
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 api.EncodingEnum) string {
func format2ContentType(format string) string {
	ct, ok := mapFormat2CT[format]
	if !ok {
		return "application/octet-stream"
	}
	return ct
}

Deleted 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
















































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

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (api *API) MakePostCreateZettelHandler(createZettel usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zettel, err := buildZettelFromData(r, id.Invalid)
		if err != nil {
			adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return
		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		u := api.NewURLBuilder('z').SetZid(newZid).String()
		h := w.Header()
		h.Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON))
		h.Set(zsapi.HeaderLocation, u)
		w.WriteHeader(http.StatusCreated)
		if err = encodeJSONData(w, zsapi.ZidJSON{ID: newZid.String(), URL: u}); err != nil {
			adapter.InternalServerError(w, "Write JSON", err)
		}
	}
}

Deleted 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





































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

// MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (api *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 {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

Changes to web/adapter/api/get_links.go.

8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25

















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

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

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73




74
75
76
77

78
79
80

81
82
83
84
85
86
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102
103
104
105
106
107
108


109
110
111
112
113
114
115

116
117
118
119
120
121
122
8
9
10
11
12
13
14
15
16
17
18

19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

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

67
68
69
70
71
72







73
74
75
76
77
78
79
80


81
82
83
84
85
86
87

88
89


90
91
92
93
94
95
96
97
98
99
100
101
102
103

104
105
106
107
108
109
110
111
112
113
114
115
116


117
118
119
120
121
122
123
124

125
126
127
128
129
130
131
132







+



-



-



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











-
+













-
+





-
-
-
-
-
-
-








-
-
+
+
+
+



-
+

-
-
+













-
+












-
-
+
+






-
+







// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"encoding/json"
	"net/http"
	"strconv"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"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(meta.KeySyntax))
		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 := zsapi.ZettelLinksJSON{
		outData := jsonGetLinks{
			ID:  zid.String(),
			URL: api.NewURLBuilder('z').SetZid(zid).String(),
		}
		if kind&kindLink != 0 {
			api.setupLinkJSONRefs(summary, matter, &outData)
			if matter&matterMeta != 0 {
				for _, p := range zn.Meta.PairsRest(false) {
					if meta.Type(p.Key) == meta.TypeURL {
						outData.Links.Meta = append(outData.Links.Meta, p.Value)
					}
				}
			}
		}
		if kind&kindImage != 0 {
			api.setupImageJSONRefs(summary, matter, &outData)
		}
		if kind&kindCite != 0 {
			outData.Cites = stringCites(summary.Cites)
		}

		w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON))
		encodeJSONData(w, outData)
		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 *zsapi.ZettelLinksJSON) {
func (api *API) setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) {
	if matter&matterIncoming != 0 {
		// TODO: calculate incoming links from other zettel (via "backward" metadata?)
		outData.Links.Incoming = []zsapi.ZidJSON{}
		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 *zsapi.ZettelLinksJSON) {
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) []zsapi.ZidJSON {
	result := make([]zsapi.ZidJSON, 0, len(refs))
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, zsapi.ZidJSON{ID: path, URL: ub.String()})
		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 {
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
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







-



-
+




-

-
-
+
+








const (
	_ matterType = 1 << iota
	matterIncoming
	matterOutgoing
	matterLocal
	matterExternal
	matterMeta
)

var mapMatter = map[string]matterType{
	"":         matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta,
	"":         matterIncoming | matterOutgoing | matterLocal | matterExternal,
	"incoming": matterIncoming,
	"outgoing": matterOutgoing,
	"local":    matterLocal,
	"external": matterExternal,
	"meta":     matterMeta,
	"zettel":   matterIncoming | matterOutgoing,
	"material": matterLocal | matterExternal | matterMeta,
	"all":      matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta,
	"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 {

Changes to web/adapter/api/get_order.go.

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

35
36
37
38
39
40
41
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40







-















-
+







// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"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(meta.KeySyntax))
		start, metas, err := zettelOrder.Run(ctx, zid, q.Get("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		api.writeMetaList(w, start, metas)
	}
}

Changes to web/adapter/api/get_role_list.go.

11
12
13
14
15
16
17
18
19


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

34
35
36
37



38
39

40
41














42
43









11
12
13
14
15
16
17


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34



35
36
37
38

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


56
57
58
59
60
61
62
63
64







-
-
+
+













-
+

-
-
-
+
+
+

-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
// Package api provides api handlers for web requests.
package api

import (
	"fmt"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/encoder"
	"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, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case zsapi.EncoderJSON:
			w.Header().Set(zsapi.HeaderContentType, format2ContentType(format))
			encodeJSONData(w, zsapi.RoleListJSON{Roles: roleList})
		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", formatText))
			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()
}

Changes to web/adapter/api/get_tags_list.go.

10
11
12
13
14
15
16

17
18
19
20


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

36
37
38
39
40
41
42
43
44
45
46
47
48































49
50
51
52












10
11
12
13
14
15
16
17
18
19


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

36
37












38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68




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







+


-
-
+
+














-
+

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

// Package api provides api handlers for web requests.
package api

import (
	"fmt"
	"net/http"
	"sort"
	"strconv"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/encoder"
	"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, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case zsapi.EncoderJSON:
			w.Header().Set(zsapi.HeaderContentType, format2ContentType(format))
			tagMap := make(map[string][]string, len(tagData))
			for tag, metaList := range tagData {
				zidList := make([]string, 0, len(metaList))
				for _, m := range metaList {
					zidList = append(zidList, m.Zid.String())
				}
				tagMap[tag] = zidList
			}
			encodeJSONData(w, zsapi.TagListJSON{Tags: tagMap})
		default:
		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 {
			adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", formatText))
		}
	}
}
				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.

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

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



44
45

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


59
60
61
62
63
64
65
12
13
14
15
16
17
18

19

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



40
41
42
43

44
45
46
47
48
49
50
51
52
53
54
55


56
57
58
59
60
61
62
63
64







-

-




+















-
-
-
+
+
+

-
+











-
-
+
+







package api

import (
	"errors"
	"fmt"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"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/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel.
func (api *API) MakeGetZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) 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()
		format, _ := adapter.GetFormat(r, q, encoder.GetDefaultFormat())
		if format == zsapi.EncoderRaw {
			ctx = box.NoEnrichContext(ctx)
		format := adapter.GetFormat(r, q, encoder.GetDefaultFormat())
		if format == "raw" {
			ctx = place.NoEnrichContext(ctx)
		}
		zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax))
		zn, err := parseZettel.Run(ctx, zid, q.Get("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		part := getPart(q, partZettel)
		if part == partUnknown {
			adapter.BadRequest(w, "Unknown _part parameter")
			return
		}
		switch format {
		case zsapi.EncoderJSON, zsapi.EncoderDJSON:
			w.Header().Set(zsapi.HeaderContentType, format2ContentType(format))
		case "json", "djson":
			w.Header().Set(adapter.ContentType, format2ContentType(format))
			err = api.getWriteMetaZettelFunc(ctx, format, part, partZettel, getMeta)(w, zn)
			if err != nil {
				adapter.InternalServerError(w, "Write D/JSON", err)
			}
			return
		}

87
88
89
90
91
92
93
94

95
96
97
98
99
100
101


102
103
104
105
106
107
108
109


110
111

112
113
114
115
116
117
118
119
120
121
122


123
124

125
126
127

128
129
130
86
87
88
89
90
91
92

93
94
95
96
97
98


99
100
101
102
103
104
105
106


107
108
109

110
111
112
113
114
115
116
117
118
119


120
121
122

123
124
125

126
127
128
129







-
+





-
-
+
+






-
-
+
+

-
+









-
-
+
+

-
+


-
+



				return
			}
			adapter.InternalServerError(w, "Get zettel", err)
		}
	}
}

func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error {
func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format string, env encoder.Environment) error {
	enc := encoder.Create(format, &env)
	if enc == nil {
		return adapter.ErrNoSuchFormat
	}
	inhMeta := false
	if format != zsapi.EncoderRaw {
		w.Header().Set(zsapi.HeaderContentType, format2ContentType(format))
	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 zsapi.EncodingEnum) error {
	w.Header().Set(zsapi.HeaderContentType, format2ContentType(format))
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 == zsapi.EncoderRaw {
		if format == "raw" {
			_, err := enc.WriteMeta(w, zn.Meta)
			return err
		}
		_, err := enc.WriteMeta(w, zn.InhMeta)
		return err
	}
	return adapter.ErrNoSuchFormat
}

func (api *API) writeZettelPartContent(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error {
	if format == zsapi.EncoderRaw {
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(zsapi.HeaderContentType, ct)
			w.Header().Add(adapter.ContentType, ct)
		}
	} else {
		w.Header().Set(zsapi.HeaderContentType, format2ContentType(format))
		w.Header().Set(adapter.ContentType, format2ContentType(format))
	}
	return writeContent(w, zn, format, &env)
}

Changes to web/adapter/api/get_zettel_context.go.

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


34
35
36
37

38
39
40
41
42
43
44
45
46
47
48
49
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30


31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
47
48







-














-
-
+
+



-
+













// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
func (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 := adapter.GetZCDirection(q.Get(zsapi.QueryKeyDir))
		depth, ok := adapter.GetInteger(q, zsapi.QueryKeyDepth)
		dir := usecase.ParseZCDirection(q.Get("dir"))
		depth, ok := adapter.GetInteger(q, "depth")
		if !ok || depth < 0 {
			depth = 5
		}
		limit, ok := adapter.GetInteger(q, zsapi.QueryKeyLimit)
		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:])
	}
}

Changes to web/adapter/api/get_zettel_list.go.

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

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

38
39
40
41
42
43
44
45


46
47
48
49
50
51
52
53

54
55

56
57

58
59
60


61
62

63
64
65
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80

81
82
83
84
85
86
87
11
12
13
14
15
16
17


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

36
37
38
39
40
41
42


43
44
45
46
47
48
49
50
51

52
53

54
55

56
57


58
59
60

61
62
63
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78

79
80
81
82
83
84
85
86







-
-



+














-
+






-
-
+
+







-
+

-
+

-
+

-
-
+
+

-
+










-
+






-
+







// Package api provides api handlers for web requests.
package api

import (
	"fmt"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/box"
	"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, formatText := adapter.GetFormat(r, q, encoder.GetDefaultFormat())
		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 == zsapi.EncoderHTML || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) {
			ctx1 = box.NoEnrichContext(ctx1)
		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(zsapi.HeaderContentType, format2ContentType(format))
		w.Header().Set(adapter.ContentType, format2ContentType(format))
		switch format {
		case zsapi.EncoderHTML:
		case "html":
			api.renderListMetaHTML(w, metaList)
		case zsapi.EncoderJSON, zsapi.EncoderDJSON:
		case "json", "djson":
			api.renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel)
		case zsapi.EncoderNative, zsapi.EncoderRaw, zsapi.EncoderText, zsapi.EncoderZmk:
			adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", formatText))
		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", formatText))
			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), zsapi.EncoderHTML, &env)
		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(zsapi.QueryKeyFormat, zsapi.FormatHTML).String(),
			api.NewURLBuilder('z').SetZid(m.Zid).AppendQuery("_format", "html").String(),
			"\">",
			htmlTitle,
			"</a></li>\n")
	}
	buf.WriteString("</ul>\n</body>\n</html>")
	buf.Flush()
}

Changes to web/adapter/api/json.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29






















30
31
32
33
34











35
36
37
38
39
40
41
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51




52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69







-









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

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








import (
	"context"
	"encoding/json"
	"io"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"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  string `json:"content"`
	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\":\"")
54
55
56
57
58
59
60
61

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

75
76
77
78
79
80
81
82
83
84
85
86
87
88

89
90
91
92
93
94
95
96
97
98
99
100
101

102
103
104
105
106
107
108
109







-
+












-
+







	}
	if 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, zsapi.FormatDJSON)
			_, 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 zsapi.EncodingEnum,
	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)
92
93
94
95
96
97
98
99

100
101
102
103
104
105
106
107
108
109
110
111

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

128
129

130
131
132


133
134
135
136
137
138
139
140
141

142
143
144
145
146
147
148
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
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135
136
137
138

139
140
141
142
143
144
145
146
147
148
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







-
+











-
+















-
+

-
+

-
-
+
+








-
+




















-
+
-
-
+








-
-
+
+

-
+






-
+




















-
+

-
+

-
+

















-
+
-
-
+








-
-
+
+

-
+







		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: domain.NewContent(""),
				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 zsapi.EncodingEnum,
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 zsapi.EncodingEnum,
func (api *API) getWriteZettelFunc(ctx context.Context, format string,
	defPart partType, getMeta usecase.GetMeta) writeZettelFunc {
	if format == zsapi.EncoderJSON {
	if format == "json" {
		return func(w io.Writer, zn *ast.ZettelNode) error {
			content, encoding := zn.Content.Encode()
			return encodeJSONData(w, zsapi.ZettelJSON{
			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(zsapi.EncoderDJSON, nil)
	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, zsapi.EncoderDJSON, &encoder.Environment{
		err = writeContent(w, zn, "djson", &encoder.Environment{
			LinkAdapter: adapter.MakeLinkAdapter(
				ctx, api, 'z', getMeta, partZettel.DefString(defPart), zsapi.EncoderDJSON),
			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 zsapi.EncodingEnum) writeZettelFunc {
	if format == zsapi.EncoderJSON {
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, zsapi.ZidMetaJSON{
			return encodeJSONData(w, jsonMeta{
				ID:   zn.Zid.String(),
				URL:  api.NewURLBuilder('z').SetZid(zn.Zid).String(),
				Meta: zn.InhMeta.Map(),
			})
		}
	}
	enc := encoder.Create(zsapi.EncoderDJSON, nil)
	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 zsapi.EncodingEnum,
func (api *API) getWriteContentFunc(ctx context.Context, format string,
	defPart partType, getMeta usecase.GetMeta) writeZettelFunc {
	if format == zsapi.EncoderJSON {
	if format == "json" {
		return func(w io.Writer, zn *ast.ZettelNode) error {
			content, encoding := zn.Content.Encode()
			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, zsapi.EncoderDJSON, &encoder.Environment{
		err = writeContent(w, zn, "djson", &encoder.Environment{
			LinkAdapter: adapter.MakeLinkAdapter(
				ctx, api, 'z', getMeta, partContent.DefString(defPart), zsapi.EncoderDJSON),
			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 zsapi.EncodingEnum) writeZettelFunc {
	if format == zsapi.EncoderJSON {
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, zsapi.ZidJSON{
			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)
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
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







-
+
















-
-
+
-
-
-
-
-
-



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

+
-
+

	}
	if err == nil {
		_, err = w.Write(jsonListFooter)
	}
	return err
}

func writeContent(w io.Writer, zn *ast.ZettelNode, format zsapi.EncodingEnum, env *encoder.Environment) error {
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 {
	outList := make([]zsapi.ZidMetaJSON, len(metaList))
	for i, m := range metaList {
	outData := jsonMetaList{
		outList[i].ID = m.Zid.String()
		outList[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String()
		outList[i].Meta = m.Map()
	}
	w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON))
	return encodeJSONData(w, zsapi.ZidMetaRelatedList{
		ID:   m.Zid.String(),
		URL:  api.NewURLBuilder('z').SetZid(m.Zid).String(),
		Meta: m.Map(),
		List: outList,
		List: make([]jsonMeta, len(metaList)),
	})
}

	}
	for i, m := range metaList {
func buildZettelFromData(r *http.Request, zid id.Zid) (domain.Zettel, error) {
	var zettel domain.Zettel
	dec := json.NewDecoder(r.Body)
	var zettelData zsapi.ZettelDataJSON
		outData.List[i].ID = m.Zid.String()
	if err := dec.Decode(&zettelData); err != nil {
		return zettel, err
	}
	m := meta.New(zid)
	for k, v := range zettelData.Meta {
		outData.List[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String()
		m.Set(k, v)
	}
	zettel.Meta = m
		outData.List[i].Meta = m.Map()
	if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil {
		return zettel, err
	}
	w.Header().Set(adapter.ContentType, format2ContentType("json"))
	return zettel, nil
	return encodeJSONData(w, outData)
}

Changes to web/adapter/api/login.go.

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


27
28
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

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





66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
94
95
96

97
98
99
12
13
14
15
16
17
18

19
20
21
22
23


24
25
26
27

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

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

88
89
90
91
92
93
94
95
96
97
98

99
100
101
102







-





-
-
+
+


-
+


















-
+
















-
+
+
+
+
+



















-
+










-
+



package api

import (
	"encoding/json"
	"net/http"
	"time"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (api *API) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc {
// MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API.
func (api *API) MakePostLoginHandlerAPI(ucAuth usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !api.withAuth() {
			w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON))
			w.Header().Set(adapter.ContentType, format2ContentType("json"))
			writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
			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)
			if err != nil {
				adapter.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(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON))
		w.Header().Set(adapter.ContentType, format2ContentType("json"))
		writeJSONToken(w, string(token), api.tokenLifetime)
	}
}

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(zsapi.AuthJSON{
	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 {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		authData := api.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(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON))
			w.Header().Set(adapter.ContentType, format2ContentType("json"))
			writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime)
			return
		}

		// Token is a little bit aged. Create a new one
		token, err := api.getToken(authData.User)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON))
		w.Header().Set(adapter.ContentType, format2ContentType("json"))
		writeJSONToken(w, string(token), api.tokenLifetime)
	}
}

Deleted web/adapter/api/rename_zettel.go.

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







































































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

	"zettelstore.de/z/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeRenameZettelHandler creates a new HTTP handler to update a zettel.
func (api *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 {
			adapter.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, ok := getZidFromURL(value); ok {
				return zid, true
			}
		}
	}
	return id.Invalid, false
}

var zidLength = len(id.VersionZid.Bytes())

func getZidFromURL(val string) (id.Zid, bool) {
	u, err := url.Parse(val)
	if err != nil {
		return id.Invalid, false
	}
	if len(u.Path) < zidLength {
		return id.Invalid, false
	}
	zid, err := id.Parse(u.Path[len(u.Path)-zidLength:])
	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
1
2
3
4
5
6
7
8
9
10
11
12
13

14




15
16
17
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 api provides api handlers for web requests.
package api

import (
import "net/url"
	"net/url"

	"zettelstore.de/z/api"
)

type partType int

const (
	partUnknown partType = iota
	partID
	partMeta
	partContent
	partZettel
)

var partMap = map[string]partType{
	api.PartID:      partID,
	api.PartMeta:    partMeta,
	api.PartContent: partContent,
	api.PartZettel:  partZettel,
	"id":      partID,
	"meta":    partMeta,
	"content": partContent,
	"zettel":  partZettel,
}

func getPart(q url.Values, defPart partType) partType {
	p := q.Get(api.QueryKeyPart)
	p := q.Get("_part")
	if p == "" {
		return defPart
	}
	if part, ok := partMap[p]; ok {
		return part
	}
	return partUnknown

Deleted 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









































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

// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (api *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
		}
		zettel, err := buildZettelFromData(r, zid)
		if err != nil {
			adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return
		}
		if err := updateZettel.Run(r.Context(), zettel, true); err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

Changes to web/adapter/encoding.go.

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



24
25
26
27
28
29
30
31
32

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

53
54
55
56
57
58
59
60
12
13
14
15
16
17
18

19



20
21
22
23
24
25
26
27
28
29
30

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

51

52
53
54
55
56
57
58







-

-
-
-
+
+
+








-
+



















-
+
-







package adapter

import (
	"context"
	"errors"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"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 api.EncodingEnum, env *encoder.Environment) (string, error) {
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 string,
	part, format string,
	format api.EncodingEnum,
) 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 {
68
69
70
71
72
73
74
75
76


77
78

79
80
81
82
83
84
85
86
87

88
89
90


91
92
93
94
95
96
97
66
67
68
69
70
71
72


73
74
75

76
77
78
79
80
81
82
83
84

85
86


87
88
89
90
91
92
93
94
95







-
-
+
+

-
+








-
+

-
-
+
+







		if origRef.State != ast.RefStateZettel {
			return origLink
		}
		zid, err := id.Parse(origRef.URL.Path)
		if err != nil {
			panic(err)
		}
		_, err = getMeta.Run(box.NoEnrichContext(ctx), zid)
		if errors.Is(err, &box.ErrNotAllowed{}) {
		_, err = getMeta.Run(place.NoEnrichContext(ctx), zid)
		if errors.Is(err, &place.ErrNotAllowed{}) {
			return &ast.FormatNode{
				Kind:    ast.FormatSpan,
				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(api.QueryKeyPart, part)
				ub.AppendQuery("_part", part)
			}
			if format != api.EncoderUnknown {
				ub.AppendQuery(api.QueryKeyFormat, format.String())
			if format != "" {
				ub.AppendQuery("_format", format)
			}
			if fragment := origRef.URL.EscapedFragment(); fragment != "" {
				ub.SetFragment(fragment)
			}

			newRef = ast.ParseReference(ub.String())
			newRef.State = ast.RefStateFound
115
116
117
118
119
120
121
122

123
124
125
126
127
128
129
113
114
115
116
117
118
119

120
121
122
123
124
125
126
127







-
+







		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(box.NoEnrichContext(ctx), zid)
			_, 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
	}

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

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



54
55
56


57
58

59
60
61


62
63
64


65
66

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


83
84
85
86
87
88
89
90
91

92
93
94
95
96
97
98
8
9
10
11
12
13
14

15
16
17
18
19

20
21

22
















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


39
40
41

42
43


44
45
46


47
48
49

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


65
66
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82







-





-


-

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












+
+
+

-
-
+
+

-
+

-
-
+
+

-
-
+
+

-
+














-
-
+
+








-
+







// under this license.
//-----------------------------------------------------------------------------

// Package adapter provides handlers for web requests.
package adapter

import (
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
	"zettelstore.de/z/usecase"
)

// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()
	if err != nil {
		log.Println(err)
		return "", "", false
	}

	ident = strings.TrimSpace(r.PostFormValue("username"))
	cred = r.PostFormValue("password")
	if ident == "" {
		return "", "", false
	}
	return ident, cred, true
}

// GetInteger returns the integer value of the named query key.
func GetInteger(q url.Values, key string) (int, bool) {
	s := q.Get(key)
	if s != "" {
		if val, err := strconv.Atoi(s); err == nil {
			return val, true
		}
	}
	return 0, false
}

// ContentType defines the HTTP header value "Content-Type".
const ContentType = "Content-Type"

// GetFormat returns the data format selected by the caller.
func GetFormat(r *http.Request, q url.Values, defFormat api.EncodingEnum) (api.EncodingEnum, string) {
	format := q.Get(api.QueryKeyFormat)
func GetFormat(r *http.Request, q url.Values, defFormat string) string {
	format := q.Get("_format")
	if len(format) > 0 {
		return api.Encoder(format), format
		return format
	}
	if format, ok := getOneFormat(r, api.HeaderAccept); ok {
		return api.Encoder(format), format
	if format, ok := getOneFormat(r, "Accept"); ok {
		return format
	}
	if format, ok := getOneFormat(r, api.HeaderContentType); ok {
		return api.Encoder(format), format
	if format, ok := getOneFormat(r, ContentType); ok {
		return format
	}
	return defFormat, "*default*"
	return defFormat
}

func getOneFormat(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {
		for _, value := range values {
			if format, ok := contentType2format(value); ok {
				return format, true
			}
		}
	}
	return "", false
}

var mapCT2format = map[string]string{
	"application/json": api.FormatJSON,
	"text/html":        api.FormatHTML,
	"application/json": "json",
	"text/html":        "html",
}

func contentType2format(contentType string) (string, bool) {
	// TODO: only check before first ';'
	format, ok := mapCT2format[contentType]
	return format, ok
}

// GetSearch retrieves the specified search and sorting options from a query.
// 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:
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
138
139
140
141
142
143
144


















-
-
-
-
-
-
-
-
-
-
-

func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search {
	for _, val := range values {
		s = s.AddExpr(key, val)
	}
	return s
}

// GetZCDirection returns a direction value for a given string.
func GetZCDirection(s string) usecase.ZettelContextDirection {
	switch s {
	case api.DirBackward:
		return usecase.ZettelContextBackward
	case api.DirForward:
		return usecase.ZettelContextForward
	}
	return usecase.ZettelContextBoth
}

Changes to web/adapter/response.go.

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27







-
+








import (
	"errors"
	"fmt"
	"log"
	"net/http"

	"zettelstore.de/z/box"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
)

// ReportUsecaseError returns an appropriate HTTP status code for errors in use cases.
func ReportUsecaseError(w http.ResponseWriter, err error) {
	code, text := CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
38
39
40
41
42
43
44
45

46
47
48

49
50
51

52
53
54
55
56
57
58
59
60

61
62
63

64
65
66
67
38
39
40
41
42
43
44

45
46
47

48
49
50

51
52
53
54
55
56
57
58
59

60
61
62

63
64
65
66
67







-
+


-
+


-
+








-
+


-
+




// NewErrBadRequest creates an new bad request error.
func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} }

func (err *ErrBadRequest) Error() string { return err.Text }

// CodeMessageFromError returns an appropriate HTTP status code and text from a given error.
func CodeMessageFromError(err error) (int, string) {
	if err == box.ErrNotFound {
	if err == place.ErrNotFound {
		return http.StatusNotFound, http.StatusText(http.StatusNotFound)
	}
	if err1, ok := err.(*box.ErrNotAllowed); ok {
	if err1, ok := err.(*place.ErrNotAllowed); ok {
		return http.StatusForbidden, err1.Error()
	}
	if err1, ok := err.(*box.ErrInvalidID); ok {
	if err1, ok := err.(*place.ErrInvalidID); ok {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid)
	}
	if err1, ok := err.(*usecase.ErrZidInUse); ok {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid)
	}
	if err1, ok := err.(*ErrBadRequest); ok {
		return http.StatusBadRequest, err1.Text
	}
	if errors.Is(err, box.ErrStopped) {
	if errors.Is(err, place.ErrStopped) {
		return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err)
	}
	if errors.Is(err, box.ErrConflict) {
	if errors.Is(err, place.ErrConflict) {
		return http.StatusConflict, "Zettelstore operations conflicted"
	}
	return http.StatusInternalServerError, err.Error()
}

Changes to web/adapter/webui/create_zettel.go.

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

28
29
30
31
32
33
34
12
13
14
15
16
17
18


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







-
-







+







package webui

import (
	"context"
	"fmt"
	"net/http"

	"zettelstore.de/z/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetCopyZettelHandler creates a new HTTP handler to display the
// HTML edit view of a copied zettel.
func (wui *WebUI) MakeGetCopyZettelHandler(getZettel usecase.GetZettel, copyZettel usecase.CopyZettel) http.HandlerFunc {
65
66
67
68
69
70
71
72

73
74
75
76
77
78

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

95
96

97
98
99
100

101
102

103
104

105
106
107
108
109
110
111
64
65
66
67
68
69
70

71
72
73
74
75
76

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

93
94

95
96
97
98

99
100

101
102

103
104
105
106
107
108
109
110







-
+





-
+















-
+

-
+



-
+

-
+

-
+







		origZettel, err := getOrigZettel(ctx, w, r, getZettel, "New")
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := origZettel.Meta
		title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk)
		textTitle, err := adapter.FormatInlines(title, api.EncoderText, nil)
		textTitle, err := adapter.FormatInlines(title, "text", nil)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)}
		htmlTitle, err := adapter.FormatInlines(title, api.EncoderHTML, &env)
		htmlTitle, err := adapter.FormatInlines(title, "html", &env)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.renderZettelForm(w, r, newZettel.Run(origZettel), textTitle, htmlTitle)
	}
}

func getOrigZettel(
	ctx context.Context,
	w http.ResponseWriter,
	r *http.Request,
	getZettel usecase.GetZettel,
	op string,
) (domain.Zettel, error) {
	if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML {
	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, formatText))
			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{}, box.ErrNotFound
		return domain.Zettel{}, place.ErrNotFound
	}
	origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
	origZettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid)
	if err != nil {
		return domain.Zettel{}, box.ErrNotFound
		return domain.Zettel{}, place.ErrNotFound
	}
	return origZettel, nil
}

func (wui *WebUI) renderZettelForm(
	w http.ResponseWriter,
	r *http.Request,

Changes to web/adapter/webui/delete_zettel.go.

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

23
24
25
26
27
28
29
30
31
32

33
34

35
36
37
38
39
40

41
42
43
44
45
46
47
11
12
13
14
15
16
17


18
19
20
21
22
23
24
25
26
27
28
29
30

31
32

33
34
35
36
37
38

39
40
41
42
43
44
45
46







-
-



+









-
+

-
+





-
+







// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"net/http"

	"zettelstore.de/z/api"
	"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/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML {
		if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Delete zettel not possible in format %q", formatText)))
				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, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		zettel, err := getZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
78
79
80
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76
77
78
79







-
+











// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		if err := deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}

Changes to web/adapter/webui/edit_zettel.go.

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

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

35
36
37
38

39
40
41
42
43
44

45
46

47
48
49
50
51
52
53
11
12
13
14
15
16
17


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34
35
36

37
38
39
40
41
42

43
44

45
46
47
48
49
50
51
52







-
-



+











-
+



-
+





-
+

-
+







// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"net/http"

	"zettelstore.de/z/api"
	"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/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		zettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML {
		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, formatText)))
				fmt.Sprintf("Edit zettel %q not possible in format %q", zid, format)))
			return
		}

		user := wui.getUser(ctx)
		m := zettel.Meta
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base)
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80







-
+







// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		zettel, hasContent, err := parseZettelForm(r, zid)
		if err != nil {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form"))
			return

Changes to web/adapter/webui/get_info.go.

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

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57

58
59

60
61
62
63
64
65

66
67
68
69
70
71
72
8
9
10
11
12
13
14

15
16

17
18

19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

46




47
48
49

50
51

52
53
54
55
56
57

58
59
60
61
62
63
64
65







-


-


-

-






+



















-
+
-
-
-
-



-
+

-
+





-
+







// under this license.
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"fmt"
	"net/http"
	"sort"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"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/usecase"
	"zettelstore.de/z/web/adapter"
)

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(
func (wui *WebUI) MakeGetInfoHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc {
	parseZettel usecase.ParseZettel,
	getMeta usecase.GetMeta,
	getAllMeta usecase.GetAllMeta,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		if format, formatText := adapter.GetFormat(r, q, api.EncoderHTML); format != api.EncoderHTML {
		if format := adapter.GetFormat(r, q, "html"); format != "html" {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Zettel info not available in format %q", formatText)))
				fmt.Sprintf("Zettel info not available in format %q", format)))
			return
		}

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		zn, err := parseZettel.Run(ctx, zid, q.Get("syntax"))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
81
82
83
84
85
86
87
88
89

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120





















121
122
123

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145





















146
147
148

149
150
151
152
153
154
155
74
75
76
77
78
79
80


81
82
83
84
85
86
87

88
89
90





















91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111



112
113





















114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134



135
136
137
138
139
140
141
142







-
-
+






-



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

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







		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()}
		}
		shadowLinks := getShadowLinks(ctx, zid, getAllMeta)
		endnotes, err := formatBlocks(nil, api.EncoderHTML, &env)
		endnotes, err := formatBlocks(nil, "html", &env)
		if err != nil {
			endnotes = ""
		}

		textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle)
		user := wui.getUser(ctx)
		canCreate := wui.canCreate(ctx, user)
		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
			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
			HasShadowLinks bool
			ShadowLinks    []string
			Endnotes       string
			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:       canCreate,
			FolgeURL:       wui.NewURLBuilder('f').SetZid(zid).String(),
			CanCopy:        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),
			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),
			HasShadowLinks: len(shadowLinks) > 0,
			ShadowLinks:    shadowLinks,
			Endnotes:       endnotes,
			Endnotes:     endnotes,
		})
	}
}

type localLink struct {
	Valid bool
	Zid   string
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
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





















-
-
-
-
-
-
+




-
+

-
-
+
+

-
+








-
-
-
-
-
-
-
-
-
-
-
-
-
-
		locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()})
	}
	return locLinks, extLinks
}

func (wui *WebUI) infoAPIMatrix(zid id.Zid) []matrixLine {
	formats := encoder.GetFormats()
	formatTexts := make([]string, 0, len(formats))
	for _, f := range formats {
		formatTexts = append(formatTexts, f.String())
	}
	sort.Strings(formatTexts)
	defFormat := encoder.GetDefaultFormat().String()
	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(formatTexts)+1)
		row := make([]matrixElement, 0, len(formats)+1)
		row = append(row, matrixElement{part, false, ""})
		for _, format := range formatTexts {
			u.AppendQuery(api.QueryKeyPart, part)
		for _, format := range formats {
			u.AppendQuery("_part", part)
			if format != defFormat {
				u.AppendQuery(api.QueryKeyFormat, format)
				u.AppendQuery("_format", format)
			}
			row = append(row, matrixElement{format, true, u.String()})
			u.ClearQuery()
		}
		matrix = append(matrix, matrixLine{row})
	}
	return matrix
}

func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string {
	ml, err := getAllMeta.Run(ctx, zid)
	if err != nil || len(ml) < 2 {
		return nil
	}
	result := make([]string, 0, len(ml)-1)
	for _, m := range ml[1:] {
		if boxNo, ok := m.Get(meta.KeyBoxNumber); ok {
			result = append(result, boxNo)
		}
	}
	return result
}

Changes to web/adapter/webui/get_zettel.go.

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

27
28
29
30
31
32
33
34
35
36
37

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

51
52
53
54
55
56
57
58
59

60
61
62
63
64
65

66
67
68
69
70

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
12
13
14
15
16
17
18

19

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

36
37
38
39
40
41
42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57

58
59
60
61
62
63

64
65
66
67
68

69
70
71
72
73
74
75
76
77

78
79
80
81
82
83
84







-

-





+










-
+












-
+








-
+





-
+




-
+








-







package webui

import (
	"bytes"
	"net/http"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/z/ast"
	"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/encoder/encfun"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"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 {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		syntax := r.URL.Query().Get("syntax")
		zn, err := parseZettel.Run(ctx, zid, syntax)
		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, "", api.EncoderUnknown),
			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, api.EncoderHTML, &envHTML)
		metaHeader, err := formatMeta(zn.InhMeta, "html", &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		htmlTitle, err := adapter.FormatInlines(
			encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), api.EncoderHTML, &envHTML)
			encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), "html", &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		htmlContent, err := formatBlocks(zn.Ast, api.EncoderHTML, &envHTML)
		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)
		canCreate := wui.canCreate(ctx, user)
		getTitle := makeGetTitle(ctx, getMeta, &encoder.Environment{Lang: lang})
		extURL, hasExtURL := zn.Meta.Get(meta.KeyURL)
		backLinks := wui.formatBackLinks(zn.InhMeta, getTitle)
		var base baseData
		wui.makeBaseData(ctx, lang, textTitle, user, &base)
		base.MetaHeader = metaHeader
		wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
111
112
113
114
115
116
117
118

119
120

121
122
123
124
125
126
127
128
129
130
131
132
133
134

135
136
137
138
139
140
141
142
143
144
145
146
147
148

149
150
151
152
153
154
155
109
110
111
112
113
114
115

116
117

118
119
120
121
122
123
124
125
126
127
128
129
130
131

132
133
134
135
136
137
138
139
140
141
142
143
144
145

146
147
148
149
150
151
152
153







-
+

-
+













-
+













-
+







			EditURL:       wui.NewURLBuilder('e').SetZid(zid).String(),
			Zid:           zid.String(),
			InfoURL:       wui.NewURLBuilder('i').SetZid(zid).String(),
			RoleText:      roleText,
			RoleURL:       wui.NewURLBuilder('h').AppendQuery("role", roleText).String(),
			HasTags:       len(tags) > 0,
			Tags:          tags,
			CanCopy:       canCreate && !zn.Content.IsBinary(),
			CanCopy:       base.CanCreate && !zn.Content.IsBinary(),
			CopyURL:       wui.NewURLBuilder('c').SetZid(zid).String(),
			CanFolge:      canCreate,
			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,
			HasExtURL:     hasExtURL,
			ExtNewWindow:  htmlAttrNewWindow(envHTML.NewWindow && hasExtURL),
			Content:       htmlContent,
			HasBackLinks:  len(backLinks) > 0,
			BackLinks:     backLinks,
		})
	}
}

func formatBlocks(bs ast.BlockSlice, format api.EncodingEnum, env *encoder.Environment) (string, error) {
func formatBlocks(bs ast.BlockSlice, format string, env *encoder.Environment) (string, error) {
	enc := encoder.Create(format, env)
	if enc == nil {
		return "", adapter.ErrNoSuchFormat
	}

	var content strings.Builder
	_, err := enc.WriteBlocks(&content, bs)
	if err != nil {
		return "", err
	}
	return content.String(), nil
}

func formatMeta(m *meta.Meta, format api.EncodingEnum, env *encoder.Environment) (string, error) {
func formatMeta(m *meta.Meta, format string, env *encoder.Environment) (string, error) {
	enc := encoder.Create(format, env)
	if enc == nil {
		return "", adapter.ErrNoSuchFormat
	}

	var content strings.Builder
	_, err := enc.WriteMeta(&content, m)
188
189
190
191
192
193
194
195

196
197
198
199
200
201
202
203
204
205
186
187
188
189
190
191
192

193
194
195
196
197
198
199
200
201
202
203







-
+










	}
	result := make([]simpleLink, 0, len(values))
	for _, val := range values {
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTitle(zid, api.EncoderText); found > 0 {
		if title, found := getTitle(zid, "text"); found > 0 {
			url := wui.NewURLBuilder('h').SetZid(zid).String()
			if title == "" {
				result = append(result, simpleLink{Text: val, URL: url})
			} else {
				result = append(result, simpleLink{Text: title, URL: url})
			}
		}
	}
	return result
}

Changes to web/adapter/webui/home.go.

12
13
14
15
16
17
18
19
20
21



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

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

51
52
53
54
55
56
12
13
14
15
16
17
18



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

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

50
51
52
53
54
55
56







-
-
-
+
+
+












-
+















-
+






package webui

import (
	"context"
	"errors"
	"net/http"

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

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

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if r.URL.Path != "/" {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}
		homeZid := wui.rtConfig.GetHomeZettel()
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetMeta(ctx, homeZid); err == nil {
				redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
				return
			}
			homeZid = id.DefaultHomeZid
		}
		_, err := s.GetMeta(ctx, homeZid)
		if err == nil {
			redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
			return
		}
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil {
		if errors.Is(err, &place.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil {
			redirectFound(w, r, wui.NewURLBuilder('a'))
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('h'))
	}
}

Changes to web/adapter/webui/htmlmeta.go.

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

28
29
30
31
32
33
34
15
16
17
18
19
20
21


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







-
-




+







	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"time"

	"zettelstore.de/z/api"
	"zettelstore.de/z/box"
	"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/z/usecase"
	"zettelstore.de/z/web/adapter"
)

var space = []byte{' '}

43
44
45
46
47
48
49
50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70


71
72
73
74
75
76
77
42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78







-
+




















+
+







	case meta.TypeID:
		wui.writeIdentifier(w, m.GetDefault(key, "???i"), getTitle)
	case meta.TypeIDSet:
		if l, ok := m.GetList(key); ok {
			wui.writeIdentifierSet(w, l, getTitle)
		}
	case meta.TypeNumber:
		wui.writeNumber(w, key, m.GetDefault(key, "???n"))
		writeNumber(w, m.GetDefault(key, "???n"))
	case meta.TypeString:
		writeString(w, m.GetDefault(key, "???s"))
	case meta.TypeTagSet:
		if l, ok := m.GetList(key); ok {
			wui.writeTagSet(w, key, l)
		}
	case meta.TypeTimestamp:
		if ts, ok := m.GetTime(key); ok {
			writeTimestamp(w, ts)
		}
	case meta.TypeURL:
		writeURL(w, m.GetDefault(key, "???u"))
	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)
		}
	case meta.TypeZettelmarkup:
		writeZettelmarkup(w, m.GetDefault(key, "???z"), env)
	case meta.TypeUnknown:
		writeUnknown(w, m.GetDefault(key, "???u"))
	default:
		strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
	}
}

func (wui *WebUI) writeHTMLBool(w io.Writer, key string, val bool) {
86
87
88
89
90
91
92
93

94
95
96
97
98
99

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

115
116
117
118
119
120
121
122
123
124


125
126
127
128
129




130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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
87
88
89
90
91
92
93

94
95
96
97
98
99

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

115
116
117
118
119
120
121
122
123


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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







-
+





-
+














-
+








-
-
+
+





+
+
+
+




















-
+

















-
+













-
+


-
-
+
+

-
+












	strfun.HTMLEscape(w, val, false)
}

func writeEmpty(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTitle getTitleFunc) {
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
	}
	title, found := getTitle(zid, api.EncoderText)
	title, found := getTitle(zid, "text")
	switch {
	case found > 0:
		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)
		}
	case found == 0:
		fmt.Fprintf(w, "<s>%v</s>", val)
	case found < 0:
		io.WriteString(w, val)
	}
}

func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTitle getTitleFunc) {
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 (wui *WebUI) writeNumber(w io.Writer, key, val string) {
	wui.writeLink(w, key, val, val)
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 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 {
			w.Write(space)
		}
		wui.writeLink(w, key, tag, tag)
	}
}

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 {
		strfun.HTMLEscape(w, val, false)
		return
	}
	fmt.Fprintf(w, "<a href=\"%v\"%v>", u, htmlAttrNewWindow(true))
	fmt.Fprintf(w, "<a href=\"%v\">", u)
	strfun.HTMLEscape(w, val, false)
	io.WriteString(w, "</a>")
}

func (wui *WebUI) writeWord(w io.Writer, key, word string) {
	wui.writeLink(w, key, word, word)
}

func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) {
	for i, word := range words {
		if i > 0 {
			w.Write(space)
		}
		wui.writeWord(w, key, word)
	}
}
func writeZettelmarkup(w io.Writer, val string, env *encoder.Environment) {
	title, err := adapter.FormatInlines(parser.ParseMetadata(val), api.EncoderHTML, env)
	title, err := adapter.FormatInlines(parser.ParseMetadata(val), "html", env)
	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
	}
	io.WriteString(w, title)
}

func (wui *WebUI) writeLink(w io.Writer, key, value, text string) {
	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, api.EncodingEnum) (string, int)
type getTitleFunc func(id.Zid, string) (string, int)

func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, env *encoder.Environment) getTitleFunc {
	return func(zid id.Zid, format api.EncodingEnum) (string, int) {
		m, err := getMeta.Run(box.NoEnrichContext(ctx), zid)
	return func(zid id.Zid, format string) (string, int) {
		m, err := getMeta.Run(place.NoEnrichContext(ctx), zid)
		if err != nil {
			if errors.Is(err, &box.ErrNotAllowed{}) {
			if errors.Is(err, &place.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 "", 1
	}
}

Changes to web/adapter/webui/lists.go.

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

28
29
30
31
32
33
34
15
16
17
18
19
20
21


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







-
-




+







	"context"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/z/box"
	"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/search"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of
// zettel as HTML.
50
51
52
53
54
55
56
57

58
59
60
61
62

63
64
65
66
67
68
69
49
50
51
52
53
54
55

56
57
58
59
60

61
62
63
64
65
66
67
68







-
+




-
+







	}
}

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("Select", s)
	title := wui.listTitleSearch("Filter", s)
	wui.renderMetaList(
		ctx, w, title, s,
		func(s *search.Search) ([]*meta.Meta, error) {
			if !s.HasComputedMetaKey() {
				ctx = box.NoEnrichContext(ctx)
				ctx = place.NoEnrichContext(ctx)
			}
			return listMeta.Run(ctx, s)
		},
		func(offset int) string {
			return wui.newPageURL('h', query, offset, "_offset", "_limit")
		})
}
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
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







-
+















-
+



-
-
-
+
+
+


















-
+

-
+

-
+







			return
		}

		title := wui.listTitleSearch("Search", s)
		wui.renderMetaList(
			ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) {
				if !s.HasComputedMetaKey() {
					ctx = box.NoEnrichContext(ctx)
					ctx = place.NoEnrichContext(ctx)
				}
				return ucSearch.Run(ctx, s)
			},
			func(offset int) string {
				return wui.newPageURL('f', query, offset, "offset", "limit")
			})
	}
}

// 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, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}
		q := r.URL.Query()
		dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir))
		depth := getIntParameter(q, api.QueryKeyDepth, 5)
		limit := getIntParameter(q, api.QueryKeyLimit, 200)
		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()
			switch dir {
			case usecase.ZettelContextBackward:
				depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward)
				depthURL.AppendQuery("dir", "backward")
			case usecase.ZettelContextForward:
				depthURL.AppendQuery(api.QueryKeyDir, api.DirForward)
				depthURL.AppendQuery("dir", "forward")
			}
			depthURL.AppendQuery(api.QueryKeyDepth, depth)
			depthURL.AppendQuery("depth", depth)
			depthLinks[i].Text = depth
			depthLinks[i].URL = depthURL.String()
		}
		var base baseData
		user := wui.getUser(ctx)
		wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
		wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct {
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
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







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










-
-
+
+
+
+
+
+
+

-
-
+
+
+
+
+
+
+







	ctx context.Context,
	w http.ResponseWriter,
	title string,
	s *search.Search,
	ucMetaList func(sorter *search.Search) ([]*meta.Meta, error),
	pageURL func(int) string) {

	var metaList []*meta.Meta
	var err error
	var prevURL, nextURL 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
		metaList, err = ucMetaList(s)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if offset := s.GetOffset(); offset > 0 {
			offset -= lps
			if offset < 0 {
				offset = 0
			}
			prevURL = pageURL(offset)
		}
		if len(metaList) >= s.GetLimit() {
			nextURL = pageURL(s.GetOffset() + lps)
			metaList = metaList[:len(metaList)-1]
		}
	} else {
		metaList, err = ucMetaList(s)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
	}
	user := wui.getUser(ctx)
	metas, err := wui.buildHTMLMetaList(metaList)
	if err != nil {
		wui.reportError(ctx, w, err)
		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
		Metas []simpleLink
		Title       string
		Metas       []simpleLink
		HasPrevNext bool
		HasPrev     bool
		PrevURL     string
		HasNext     bool
		NextURL     string
	}{
		Title: title,
		Metas: metas,
		Title:       title,
		Metas:       metas,
		HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0,
		HasPrev:     len(prevURL) > 0,
		PrevURL:     prevURL,
		HasNext:     len(nextURL) > 0,
		NextURL:     nextURL,
	})
}

func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string {
	if s == nil {
		return wui.rtConfig.GetSiteName()
	}
331
332
333
334
335
336
337
338

339
340
341
342
343
344
345
346
347
348
366
367
368
369
370
371
372

373
374
375
376
377
378
379
380
381
382
383







-
+










		if val, ok := m.Get(meta.KeyLang); ok {
			lang = val
		} else {
			lang = defaultLang
		}
		title, _ := m.Get(meta.KeyTitle)
		env := encoder.Environment{Lang: lang, Interactive: true}
		htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), api.EncoderHTML, &env)
		htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env)
		if err != nil {
			return nil, err
		}
		metas = append(metas, simpleLink{
			Text: htmlTitle,
			URL:  wui.NewURLBuilder('h').SetZid(m.Zid).String(),
		})
	}
	return metas, nil
}

Changes to web/adapter/webui/login.go.

36
37
38
39
40
41
42
43
44


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


43
44
45
46
47
48
49
50
51







-
-
+
+







		Retry bool
	}{
		Title: base.Title,
		Retry: retry,
	})
}

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user.
func (wui *WebUI) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc {
// MakePostLoginHandlerHTML creates a new HTTP handler to authenticate the given user.
func (wui *WebUI) MakePostLoginHandlerHTML(ucAuth usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !wui.authz.WithAuth() {
			redirectFound(w, r, wui.NewURLBuilder('/'))
			return
		}
		ctx := r.Context()
		ident, cred, ok := adapter.GetCredentialsViaForm(r)

Changes to web/adapter/webui/rename_zettel.go.

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

24
25
26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
45

46
47

48
49
50
51
52
53
54
12
13
14
15
16
17
18


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

34
35
36
37
38
39
40
41
42
43

44
45

46
47
48
49
50
51
52
53







-
-



+











-
+









-
+

-
+







package webui

import (
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/z/api"
	"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/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetRenameZettelHandler creates a new HTTP handler to display the
// HTML rename view of a zettel.
func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		m, err := getMeta.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML {
		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(), formatText)))
				fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), format)))
			return
		}

		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 {
63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76







-
+








// MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel.
func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		curZid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, place.ErrNotFound)
			return
		}

		if err = r.ParseForm(); err != nil {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form"))
			return
		}

Changes to web/adapter/webui/response.go.

10
11
12
13
14
15
16
17

18
19
20

21
22
10
11
12
13
14
15
16

17
18
19

20
21
22







-
+


-
+



// Package webui provides web-UI handlers for web requests.
package webui

import (
	"net/http"

	"zettelstore.de/z/api"
	"zettelstore.de/z/web/server"
)

func redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
func redirectFound(w http.ResponseWriter, r *http.Request, ub server.URLBuilder) {
	http.Redirect(w, r, ub.String(), http.StatusFound)
}

Changes to web/adapter/webui/webui.go.

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

34
35
36
37
38
39
40
41
42
43
44
45

46
47
48
49
50
51
52

53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
74

75
76
77
78
79
80

81
82
83
84
85


86
87

88
89
90
91
92
93
94
95
96

97
98
99
100
101

102
103

104
105
106
107
108
109
110
15
16
17
18
19
20
21

22

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50

51

52
53
54
55
56
57
58
59
60

61
62
63
64
65
66
67
68
69
70
71

72
73
74
75
76
77

78
79
80
81


82
83


84
85
86
87
88
89
90
91
92

93
94
95
96
97

98
99

100
101
102
103
104
105
106
107







-

-









+











-
+






-
+
-









-
+










-
+





-
+



-
-
+
+
-
-
+








-
+




-
+

-
+







	"bytes"
	"context"
	"log"
	"net/http"
	"sync"
	"time"

	"zettelstore.de/z/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"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"
)

// WebUI holds all data for delivering the web ui.
type WebUI struct {
	ab       server.AuthBuilder
	authz    auth.AuthzManager
	rtConfig config.Config
	token    auth.TokenManager
	box      webuiBox
	place    webuiPlace
	policy   auth.Policy

	templateCache map[id.Zid]*template.Template
	mxCache       sync.RWMutex

	tokenLifetime time.Duration
	cssBaseURL    string
	stylesheetURL string
	cssUserURL    string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	withAuth      bool
	loginURL      string
	searchURL     string
}

type webuiBox interface {
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
}

// New creates a new WebUI struct.
func New(ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr box.Manager, pol auth.Policy) *WebUI {
	mgr place.Manager, pol auth.Policy) *WebUI {
	wui := &WebUI{
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		place:    mgr,
		policy:   pol,

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration),
		cssBaseURL: ab.NewURLBuilder('z').SetZid(
			id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(),
		stylesheetURL: ab.NewURLBuilder('z').SetZid(
			id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery(
		cssUserURL: ab.NewURLBuilder('z').SetZid(
			id.UserCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(),
			"_part", "content").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(),
		withAuth:      authz.WithAuth(),
		loginURL:      ab.NewURLBuilder('a').String(),
		searchURL:     ab.NewURLBuilder('f').String(),
	}
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	wui.observe(place.UpdateInfo{Place: mgr, Reason: place.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) observe(ci box.UpdateInfo) {
func (wui *WebUI) observe(ci place.UpdateInfo) {
	wui.mxCache.Lock()
	if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid {
	if ci.Reason == place.OnReload || ci.Zid == id.BaseTemplateZid {
		wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache))
	} else {
		delete(wui.templateCache, ci.Zid)
	}
	wui.mxCache.Unlock()
}

119
120
121
122
123
124
125
126

127
128
129
130
131
132

133
134
135
136

137
138
139
140

141
142
143
144
145
146
147
148

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
116
117
118
119
120
121
122

123
124
125
126
127
128

129
130
131
132

133
134
135
136

137
138
139
140
141
142
143
144

145
146
147
148
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







-
+





-
+



-
+



-
+







-
+

















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





+
-
-
-
+
+
+

+
+
+
+






-


-
+
-












-
+





-
+








-
-
+
+
-
-
-
-
+



-
+
+
+





-
+









-
+

-
+







	t, ok := wui.templateCache[zid]
	wui.mxCache.RUnlock()
	return t, ok
}

func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool {
	m := meta.New(id.Invalid)
	return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx)
	return wui.policy.CanCreate(user, m) && wui.place.CanCreateZettel(ctx)
}

func (wui *WebUI) canWrite(
	ctx context.Context, user, meta *meta.Meta, content domain.Content) bool {
	return wui.policy.CanWrite(user, meta, meta) &&
		wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content})
		wui.place.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content})
}

func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool {
	return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid)
	return wui.policy.CanRename(user, m) && wui.place.AllowRenameZettel(ctx, m.Zid)
}

func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool {
	return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid)
	return wui.policy.CanDelete(user, m) && wui.place.CanDeleteZettel(ctx, m.Zid)
}

func (wui *WebUI) getTemplate(
	ctx context.Context, templateID id.Zid) (*template.Template, error) {
	if t, ok := wui.cacheGetTemplate(templateID); ok {
		return t, nil
	}
	realTemplateZettel, err := wui.box.GetZettel(ctx, templateID)
	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
	CSSBaseURL        string
	Lang           string
	MetaHeader     string
	StylesheetURL  string
	CSSUserURL        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
	HasNewZettelLinks bool
	NewZettelLinks    []simpleLink
	SearchURL         string
	Content           string
	FooterHTML        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
		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()
	}
	newZettelLinks := wui.fetchNewTemplates(ctx, user)

	data.Lang = lang
	data.CSSBaseURL = wui.cssBaseURL
	data.StylesheetURL = wui.stylesheetURL
	data.CSSUserURL = wui.cssUserURL
	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.HasNewZettelLinks = len(newZettelLinks) > 0
	data.CanCreate = canCreate
	data.NewZettelLinks = newZettelLinks
	data.SearchURL = wui.searchURL
	data.FooterHTML = wui.rtConfig.GetFooterHTML()
}

// htmlAttrNewWindow returns HTML attribute string for opening a link in a new window.
// 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 ""
}

func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) {
	ctx = box.NoEnrichContext(ctx)
func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink {
	ctx = place.NoEnrichContext(ctx)
	if !wui.canCreate(ctx, user) {
		return nil
	}
	menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
	menu, err := wui.place.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}
	refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig))
	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.box.GetMeta(ctx, zid)
		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, api.EncoderHTML, &env)
		menuTitle, err := adapter.FormatInlines(astTitle, "html", &env)
		if err != nil {
			menuTitle, err = adapter.FormatInlines(astTitle, api.EncoderText, 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(),
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
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







-
+














-
+







			wui.setToken(w, tok)
		}
	}
	var content bytes.Buffer
	err = t.Render(&content, data)
	if err == nil {
		base.Content = content.String()
		w.Header().Set(api.HeaderContentType, "text/html; charset=utf-8")
		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) *api.URLBuilder { return wui.ab.NewURLBuilder(key) }
func (wui *WebUI) NewURLBuilder(key byte) server.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)
}

Changes to web/server/impl/impl.go.

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

19
20
21
22
23
24
25







-







package impl

import (
	"context"
	"net/http"
	"time"

	"zettelstore.de/z/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/web/server"
)

type myServer struct {
	server           httpServer
54
55
56
57
58
59
60
61
62


63
64
65
66
67
68
69
53
54
55
56
57
58
59


60
61
62
63
64
65
66
67
68







-
-
+
+







}
func (srv *myServer) GetUser(ctx context.Context) *meta.Meta {
	if data := srv.GetAuthData(ctx); data != nil {
		return data.User
	}
	return nil
}
func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder {
	return api.NewURLBuilder(srv.GetURLPrefix(), key)
func (srv *myServer) NewURLBuilder(key byte) server.URLBuilder {
	return &URLBuilder{router: &srv.router, key: key}
}
func (srv *myServer) GetURLPrefix() string {
	return srv.router.urlPrefix
}

const sessionName = "zsession"

Changes to web/server/impl/router.go.

96
97
98
99
100
101
102
103

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118











119
120





121
122
123
124
125
126
127
96
97
98
99
100
101
102

103















104
105
106
107
108
109
110
111
112
113
114


115
116
117
118
119
120
121
122
123
124
125
126







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







		if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix {
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		r.URL.Path = r.URL.Path[prefixLen-1:]
	}
	match := rt.reURL.FindStringSubmatch(r.URL.Path)
	if len(match) != 3 {
	if len(match) == 3 {
		rt.mux.ServeHTTP(w, rt.addUserContext(r))
		return
	}

	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
		}
		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
			}
	}
	http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
			return
		}
	}
	rt.mux.ServeHTTP(w, rt.addUserContext(r))
}

func (rt *httpRouter) addUserContext(r *http.Request) *http.Request {
	if rt.ur == nil {
		return r
	}
	k := auth.KindJSON

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

12
13
14
15
16
17
18
19
20
21
22



























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

41
42
43

44
45
46
47
48
49
50
12
13
14
15
16
17
18




19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63
64
65

66
67
68
69
70
71
72
73







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

















-
+


-
+







package server

import (
	"context"
	"net/http"
	"time"

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

// URLBuilder builds URLs.
type URLBuilder interface {
	// Clone an URLBuilder
	Clone() URLBuilder

	// SetZid sets the zettel identifier.
	SetZid(zid id.Zid) URLBuilder

	// AppendPath adds a new path element
	AppendPath(p string) URLBuilder

	// AppendQuery adds a new query parameter
	AppendQuery(key, value string) URLBuilder

	// ClearQuery removes all query parameters.
	ClearQuery() URLBuilder

	// SetFragment stores the fragment
	SetFragment(s string) URLBuilder

	// String produces a string value.
	String() string
}

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

// Builder allows to build new URLs for the web service.
type Builder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
	NewURLBuilder(key byte) URLBuilder
}

// Auth is the authencation interface.
// Auth is.
type Auth interface {
	GetUser(context.Context) *meta.Meta
	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

Changes to www/build.md.

31
32
33
34
35
36
37
38

39
40
41
42
43
44

45
46
47
48
49
50
51
31
32
33
34
35
36
37

38
39
40
41
42
43

44
45
46
47
48
49
50
51







-
+





-
+







```

The flag `-v` enables the verbose mode.
It outputs all commands called by the tool.

`COMMAND` is one of:

* `build`: builds the software with correct version information and puts it
* `build`: builds the software with correct version information and places it
  into a freshly created directory <tt>bin</tt>.
* `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 put in the directory <tt>releases</tt>.
  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

```

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



3

4














































5
6
7

8
9

10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

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

58
59
60
61
62


63
64
65
66
67
68
69
70
71

72
73
74


75
76
77

78

79
80
81
82
83
84


85
86
87
88

89
90
91

92


93
94




95
96
97
98
99
100
101



102
103
104
105
106
107
108
109
110
111


-
-
-

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



-
+

-
+










-
+
+



















-
+















-
+




-
-
+
+







-
+


-
-
+
+

-
+
-






-
-
+
+


-
+


-
+
-
-
+

-
-
-
-
+
+
+
+



-
-
-
+
+
+







<title>Change Log</title>

<a name="0_0_15"></a>
<h2>Changes for Version 0.0.15 (pending)</h2>

<a name="0_0_14"></a>
<h2>Changes for Version 0.0.14 (2021-07-23)</h2>
<h2>Changes for Version 0.0.14 (pending)</h2>
  *  Rename &ldquo;place&rdquo; into &ldquo;box&rdquo;. This also affects the
     configuration keys to specify boxes <tt>box-uri<em>X</em></tt> (previously
     <tt>place-uri-<em>X</em></tt>. Older changes documented here are renamed
     too.
     (breaking)
  *  Add API for creating, updating, renaming, and deleting zettel.
     (major: api)
  *  Initial API client for Go.
     (major: api)
  *  Remove support for paging of WebUI list. Runtime configuration key
     <tt>list-page-size</tt> is removed. If you still specify it, it will be
     ignored.
     (major: webui)
  *  Use endpoint <tt>/v</tt> for user authentication via API. Endpoint
     <tt>/a</tt> is now used for the web user interface only. Similar, endpoint
     <tt>/y</tt> (&ldquo;zettel context&rdquo;) is renamed to <tt>/x</tt>.
     (minor, possibly breaking)
  *  Type of used-defined metadata is determined by suffix of key:
     <tt>-number</tt>, <tt>-url</tt>, <tt>-zid</tt> will result the values to
     be interpreted as a number, an URL, or a zettel identifier.
     (minor, but possibly breaking if you already used a metadata key with
     above suffixes, but as a string type)
  *  New <tt>user-role</tt> &ldquo;creator&rdquo;, which is only allowed to
     create new zettel (except user zettel). This role may only read and update
     public zettel or its own user zettel. Added to support future client
     software (e.g. on a mobile device) that automatically creates new zettel
     but, in case of a password loss, should not allow to read existing zettel.
     (minor, possibly breaking, because new zettel template zettel must always
     prepend the string <tt>new-</tt> before metdata keys that should be
     transferred to the new zettel)
  *  New suported metadata key <tt>box-number</tt>, which gives an indication
     from which box the zettel was loaded.
     (minor)
  *  New supported syntax <tt>html</tt>.
     (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 inprovements, to the software and to the
     documentation.

<a name="0_0_13"></a>
<h2>Changes for Version 0.0.13 (2021-06-01)</h2>
  *  Startup configuration <tt>box-<em>X</em>-uri</tt> (where <em>X</em> is a
  *  Startup configuration <tt>place-<em>X</em>-uri</tt> (where <em>X</em> is a
     number greater than zero) has been renamed to
     <tt>box-uri-<em>X</em></tt>.
     <tt>place-uri-<em>X</em></tt>.
     (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.
     (breaking: webui, api)
  *  Administrator console (only optional accessible locally). Enable it only
     on systems with a single user or with trusted users. It is disabled by
     default.
     (major: core)
  *  Remove visibility value &ldquo;simple-expert&rdquo; introduced in
     [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There
     was a name collision with the &ldquo;simple&rdquo; directory box sub-type.
     was a name collision with the &ldquo;simple&rdquo; directory place
     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.
     (major)
  *  Full-text search can be a prefix search or a search for equal words, in
     addition to the search whether a word just contains word of the search
     term.
     (minor: api, webui)
  *  Full-text search for URLs, with above additional operators.
     (minor: api, webui)
  *  Add system zettel about license, contributors, and dependencies (and their
     license).
     For a nicer layout of zettel identifier, the zettel about environment
     values and about runtime metrics got new zettel identifier. This affects
     only user that referenced those zettel.
     (minor)
  *  Local images that cannot be read (not found or no access rights) are
     substituted with the new default image, a spinning emoji.
     See [/file?name=box/constbox/emoji_spin.gif].
     See [/file?name=place/constplace/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.
     (minor: zmk)
  *  Many smaller bug fixes and inprovements, to the software and to the
     documentation.

<a name="0_0_12"></a>
<h2>Changes for Version 0.0.12 (2021-04-16)</h2>
  *  Raise the per-process limit of open files on macOS to 1.048.576. This
     allows most macOS users to use at least 500.000 zettel. That should be
     enough for the near future.
     (major)
  *  Mitigate the shortcomings of the macOS version by introducing types of
     directory boxes. The original directory box type is now called "notify"
     directory places. The original directory place type is now called "notify"
     (the default value). There is a new type called "simple". This new type
     does not notify Zettelstore when some of the underlying Zettel files
     change.
     (major)
  *  Add new startup configuration <tt>default-dir-box-type</tt>, which gives
     the default value for specifying a directory box type. The default value
  *  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
     &ldquo;simple&rdquo; if some errors occur while raising the per-process
     limit of open files.
     (minor)

<a name="0_0_11"></a>
<h2>Changes for Version 0.0.11 (2021-04-05)</h2>
  *  New box schema "file" allows to read zettel from a ZIP file.
  *  New place 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
     to be searched are part of words inside a zettel, both content and metadata.
     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>.
     (minor: api, webui)
  *  Menu bar is shown when displaying error messages.
     (minor: webui)
  *  When selecting zettel, it can be specified that a given value should
     <em>not</em> match. Previously, only the whole select criteria could be
  *  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
     negated (which is still possible).
     (minor: api, webui)
  *  You can select a zettel by specifying that specific metadata keys must
  *  You can filter a zettel list 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
  *  Context of a zettel (introduced in version 0.0.10) does not take tags into account any more.
     account any more. Using some tags for determining the context resulted
     into erratic, non-deterministic context lists.
     Using some tags for determining the context resulted into erratic, non-deterministic context lists.
     (minor: api, webui)
  *  Selecting zettel depending on tag values can be both by comparing only the
     prefix or the whole string. If a search value begins with '#', only zettel
     with the exact tag will be returned. Otherwise a zettel will be returned
     if the search string just matches the prefix of only one of its tags.
  *  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.
     (minor: api, webui)
  *  Many smaller bug fixes and inprovements, to the software and to the documentation.

A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
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:
  #  You update the per-process limit of open files on macOS.
  #  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>
<h2>Changes for Version 0.0.10 (2021-02-26)</h2>
  *  Menu item &ldquo;Home&rdquo; now redirects to a home zettel.
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
130
131
132
133
134
135
136

137
138

139
140
141
142
143
144
145
146
147
148
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







-
+

-
+















-
-
+
+




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

-
-
+
+
-

-
+
-




-
-
-
+
+
+
-
-
+


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

-
+
-





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


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

-
+
-



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

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


-
-
+
+
-
-
+
-

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




-
-
-
-
+
+
+
+




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







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



-
+
-

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

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



-
+
-
-
+





-
+
-









-
+
-
-
-
+
+
-

-
-
+
+





     <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/selecting ignores the leading '#' character of tags.
  *  Searching/filtering ignores the leading '#' character of tags.
     (minor: api, webui)
  *  When result of selecting or searching is presented, the query is written as the page heading.
  *  When result of filtering 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.
     (bug: server)
  *  Runtime configuration key <tt>marker-external</tt> 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.
     (minor: webui)
  *  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.

<a name="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
             Metadata key <tt>published</tt> is the first example of such a property.
             a property.
  *  (major) A background activity (called <i>indexer</i>) continuously
             monitors zettel changes to establish the reverse direction of
  *  (major) A background activity (called <i>indexer</i>) continuously monitors
             zettel changes to establish the reverse direction of found internal links.
             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
             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
             metadata for
             [https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel].
             Metadata keys with type &ldquo;Identifier&rdquo; or
             property metadata for [https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel].
             Metadata keys with type &ldquo;Identifier&rdquo; or &ldquo;IdentifierSet&rdquo;
             &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
             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.
             <tt>backward</tt>, but without forward links. Therefore,
             <tt>back</tt> is something like the list of &ldquo;smart
             Therefore, <tt>back</tt> is something like the list of &ldquo;smart backlinks&rdquo;.
             backlinks&rdquo;.
  *  (minor) If Zettelstore is being stopped, an appropriate message is written
  *  (minor) If Zettelstore is being stopped, an appropriate message is written in the console log.
             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
  *  (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).
             other variant).
  *  (minor) Remove zettel <tt>00000000000004</tt>, which contained the Go
  *  (minor) Remove zettel <tt>00000000000004</tt>, which contained the Go version that produced the Zettelstore executable.
             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
             It was too specific to the current implementation.
             This information is now included in zettel <tt>00000000000006</tt> (<i>Zettelstore Environment Values</i>).
             Environment Values</i>).
  *  (minor) Predefined templates for new zettel do not contain any value for
  *  (minor) Predefined templates for new zettel do not contain any value for attribute <tt>visibility</tt> any more.
             attribute <tt>visibility</tt> 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
             It is a non-empty string, that will be formatted with Zettelmarkup.
             <tt>title</tt> and <tt>default-title</tt> have this type.
             type.
  *  (major) Rename zettel syntax &ldquo;meta&rdquo; to &ldquo;none&rdquo;.
             Please update the <i>Zettelstore Runtime Configuration</i> and all
             Please update the <i>Zettelstore Runtime Configuration</i> and all other zettel that previously used the value &ldquo;meta&rdquo;.
             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
  *  (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.
             in random order. <tt>_order</tt> / <tt>order</tt> are now an
             aliases for the query parameters <tt>_sort</tt> / <tt>sort</tt>.
             <tt>_order</tt> / <tt>order</tt> are now an aliases for the query parameters <tt>_sort</tt> / <tt>sort</tt>.

<h3>WebUI</h3>
  *  (major) HTML template zettel for WebUI now use
  *  (major) HTML template zettel for WebUI now use [https://mustache.github.io/|Mustache]
             [https://mustache.github.io/|Mustache] syntax instead of
             previously used [https://golang.org/pkg/html/template/|Go
             syntax instead of previously used [https://golang.org/pkg/html/template/|Go template] syntax.
             template] syntax. This allows these zettel to be used, even when
             there is another Zettelstore implementation, in another
             This allows these zettel to be used, even when there is another Zettelstore implementation, in another programming language.
             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
             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
             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
             (&ldquo;Additional links to this zettel&rdquo;).
  *  (minor) All property metadata, even computed metadata is shown in the info page of a zettel.
             page of a zettel.
  *  (minor) Rendering of metadata keys <tt>title</tt> and
  *  (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.
             <tt>default-title</tt> 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
             Previously, the identifier was not shown if the zettel was not editable.
             editable.
  *  (minor) Do not show computed metadata in edit forms anymore.

<a name="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
  *  (bug) Zettel files with extension <tt>.jpg</tt> and without metadata will get a <tt>syntax</tt> value &ldquo;jpg&rdquo;.
           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
           The internal data structure got the same value internally, instead of &ldquo;jpeg&rdquo;.
           This has been fixed for all possible alternative syntax values.
           syntax values.
  *  (bug) If a file, e.g. an image file like <tt>20201130190200.jpg</tt>, is
           added to the directory box, its metadata are just calculated from
  *  (bug) If a file, e.g. an image file like <tt>20201130190200.jpg</tt>, is added to the directory place,
  *        its metadata are just calculated from the information available.
           the information available. Updated metadata did not find its way
           into the zettel box, because the <tt>.meta</tt> file was not
           written.
  *  (bug) If just the <tt>.meta</tt> file was deleted manually, the zettel was
           assumed to be missing. A workaround is to restart the software. If
           the <tt>.meta</tt> file is deleted, metadata is now calculated in
           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.
           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.
           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.
  *  (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).
           <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 boxes for the new zettel
  *  (bug) When renaming a zettel, check all places for the new zettel identifier, not just the first place.
           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
           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>.
             <tt>visibility</tt>.
  *  (bug) If <tt>list-page-size</tt> is set to a relatively small value and
  *  (bug) If <tt>list-page-size</tt> is set to a relatively small value and the authenticated user is <i>not</i> the owner,
           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.
  *        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
             An owner becomes an expert, if the runtime configuration key <tt>expert-mode</tt> is set to true.
             <tt>expert-mode</tt> 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
  *  (bug) Fixes a memory leak that results in too many open files after approx. 125 reload operations.
             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
  *  (major) Predefined templates for new zettel got an explicit value for visibility: &ldquo;login&rdquo;.
             Please update these zettel if you modified them.
             modified them.
  *  (major) Rename key <tt>readonly</tt> of <i>Zettelstore Startup
  *  (major) Rename key <tt>readonly</tt> of <i>Zettelstore Startup Configuration</i> to <tt>read-only-mode</tt>.
             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.
             This was done to avoid some confusion with the the zettel metadata key <tt>read-only</tt>.
             <b>Please adapt your startup configuration.
             Otherwise your Zettelstore will be accidentally writable.</b>
  *  (minor) References starting with &ldquo;./&rdquo; and &ldquo;../&rdquo;
  *  (minor) References starting with &ldquo;./&rdquo; and &ldquo;../&rdquo; are treated as a local reference.
             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
             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.
             current local time if a zettel is updated through Zettelstore.
             <b>If you used that key previously for your own, you should rename
             <b>If you used that key previously for your own, you should rename it before you upgrade.</b>
             it before you upgrade.</b>
  *  (minor) The new visibility value &ldquo;simple-expert&rdquo; ensures that
  *  (minor) The new visibility value &ldquo;simple-expert&rdquo; ensures that many computed zettel are shown for new users.
             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
             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.
             case the file names were corrupted. In addition, there could be
             a tool that compares the identifier with the file name.
             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
  *  (minor) Remove list of tags in &ldquo;List Zettel&rdquo; and search results.
             There was some feedback that the additional tags were not helpful.
             helpful.
  *  (minor) Move zettel field "role" above "tags" and move "syntax" more to
  *  (minor) Move zettel field "role" above "tags" and move "syntax" more to "content".
             "content".
  *  (minor) Rename zettel operation &ldquo;clone&rdquo; to &ldquo;copy&rdquo;.
  *  (major) All predefined HTML templates have now a visibility value
  *  (major) All predefined HTML templates have now a visibility value &ldquo;expert&rdquo;.
             &ldquo;expert&rdquo;. If you want to see them as an non-expert
             owner, you must temporary enable <tt>expert-mode</tt> and change
             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.
             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
  *  (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.
             zettel is created with a reference (<tt>precursor</tt>) to the
             original zettel. Title, role, tags, and syntax are copied from the
             Title, role, tags, and syntax are copied from the original zettel.
             original zettel.
  *  (major) Most predefined zettel have a title prefix of
  *  (major) Most predefined zettel have a title prefix of &ldquo;Zettelstore&rdquo;.
             &ldquo;Zettelstore&rdquo;.
  *  (minor) If started in simple mode, e.g. via double click or without any
  *  (minor) If started in simple mode, e.g. via double click or without any command, some information for the new user is presented.
             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
             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.
             more information. (This change also applies to the server itself,
             but it is more suited to the WebUI user.)
             (This change also applies to the server itself, but it is more suited to the WebUI user.)

<a name="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>
<h2>Changes for Version 0.0.6 (2020-11-23)</h2>
<h3>Server</h3>
  *  (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to
  *  (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to <tt>00000000000100</tt> (previously <tt>00000000000001</tt>).
             <tt>00000000000100</tt> (previously <tt>00000000000001</tt>). This
             is done to gain some free identifier with smaller number to be
             This is done to gain some free identifier with smaller number to be used internally.
             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
             <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>.
             <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
             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>
             more.</b>
  *  (minor) Rename the scheme of the box URL where predefined zettel are
  *  (minor) Rename the scheme of the place URL where predefined zettel are stored to &ldquo;const&rdquo;.
             stored to &ldquo;const&rdquo;. The previous value was
             &ldquo;globals&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
  *  (bug) Encoding binary content in format &ldquo;json&rdquo; now results in valid JSON content.
             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
  *  (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;.
             &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
             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).
             &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
             You can use this to provide a zettel reference that links to the heading, without specifying an explicit mark (<code>&#91;!mark]</code>).
             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
  *  (minor) Focus on the first text field on some forms (new zettel, edit zettel, rename zettel, login)
             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,
  *  (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,
             nor host name, are considered &ldquo;local references&rdquo; (in
             contrast to &ldquo;zettel references&rdquo; and &ldquo;external
             are considered &ldquo;local references&rdquo; (in contrast to &ldquo;zettel references&rdquo; and &ldquo;external references&rdquo;).
             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;
             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;.
             or &ldquo;native&rdquo;. Local references are listed on the
             <i>Info page</i> of each zettel.
  *  (minor) Change the default value for some visual sugar putd after an
             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;).
             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>.
             This affects the former key <tt>icon-material</tt> of the <i>Zettelstore Runtime Configuration</i>, which is renamed to <tt>marker-external</tt>.
  *  (major) Allow multiple zettel to act as templates for creating new zettel.
             All zettel with a role value &ldquo;new-template&rdquo; act as
             All zettel with a role value &ldquo;new-template&rdquo; act as a template to create a new zettel.
             a template to create a new zettel. The WebUI menu item
             &ldquo;New&rdquo; changed to a drop-down list with all those
             The WebUI menu item &ldquo;New&rdquo; changed to a drop-down list with all those zettel, ordered by their identifier.
             zettel, ordered by their identifier. All metadata keys with the
             prefix <tt>new-</tt> will be translated to a new or updated
             All metadata keys with the prefix <tt>new-</tt> will be translated to a new or updated keys/value without that prefix.
             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
             You can use this mechanism to specify a role for the new zettel, or a different title.
             The title of the template zettel is used in the drop-down list.
             The initial template zettel &ldquo;New Zettel&rdquo; has now a different zettel identifier
             (now: <tt>00000000091001</tt>, was: <tt>00000000040001</tt>).
             <b>Please update it, if you changed that zettel.</b>
             zettel.</b>
             <br>Note: this feature was superseded in [#0_0_10|version 0.0.10]
             <br>Note: this feature was superseded in [#0_0_10|version 0.0.10] by the &ldquo;New Menu&rdquo; zettel.
             by the &ldquo;New Menu&rdquo; zettel.
  *  (minor) When a page should be opened in a new windows (e.g. for external
  *  (minor) When a page should be opened in a new windows (e.g. for external references),
             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
             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.
             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
  *  (minor) If the value of the <i>Zettelstore Runtime Configuration</i> key <tt>list-page-size</tt> is greater than zero,
             <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 number of WebUI list elements will be restricted and it is possible to change to the next/previous page to list more elements.
             the next/previous page to list more elements.
  *  (minor) Change CSS to enhance reading: make <code>line-height</code>
  *  (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 little smaller (previous: 1.6, now 1.4) and move list items to
             the left.

<a name="0_0_5"></a>
<h2>Changes for Version 0.0.5 (2020-10-22)</h2>
  *  Application Programming Interface (API) to allow external software to
  *  Application Programming Interface (API) to allow external software to retrieve zettel data from the Zettelstore.
     retrieve zettel data from the Zettelstore.
  *  Specify boxes, where zettel are stored, via an URL.
  *  Specify places, where zettel are stored, via an URL.
  *  Add support for a custom footer.

<a name="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),
  *  New sub-commands <tt>file</tt> (use Zettelstore as a command line filter), <tt>password</tt> (for authentication), and <tt>config</tt>.
     <tt>password</tt> (for authentication), and <tt>config</tt>.

<a name="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>
<h2>Changes for Version 0.0.2 (2020-08-28)</h2>
  *  Configuration zettel now has ID <tt>00000000000001</tt> (previously:
  *  Configuration zettel now has ID <tt>00000000000001</tt> (previously: <tt>00000000000000</tt>).
     <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
  *  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.
     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 <tt>00000000040001</tt> is introduced.
     You can change it if you need a different template zettel.

<a name="0_0_1"></a>
<h2>Changes for Version 0.0.1 (2020-08-21)</h2>
  *  Initial public release.

Changes to www/download.wiki.

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

13
14
15
16
17
18





19
20
21
22
23
24
25



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

12
13





14
15
16
17
18
19
20
21
22



23
24
25












-
+

-
-
-
-
-
+
+
+
+
+




-
-
-
+
+
+
-
<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.14</code> (2021-07-23).
Build: <code>v0.0.13</code> (2021-06-01).

  *  [/uv/zettelstore-0.0.14-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.0.14-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.0.14-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.0.14-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.0.14-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/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)

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 box to read the zettel directly from the
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.
ZIP file.

Changes to www/index.wiki.

13
14
15
16
17
18
19
20

21
22
23
24
25
26





27
28
29
30
31
32
33
34
35
36
37
13
14
15
16
17
18
19

20
21





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







-
+

-
-
-
-
-
+
+
+
+
+











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://twitter.com/zettelstore|Stay tuned]&hellip;
<hr>
<h3>Latest Release: 0.0.14 (2021-07-23)</h3>
<h3>Latest Release: 0.0.13 (2021-06-01)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_0_14|Change summary]
  *  [/timeline?p=version-0.0.14&bt=version-0.0.13&y=ci|Check-ins for version 0.0.14],
     [/vdiff?to=version-0.0.14&from=version-0.0.13|content diff]
  *  [/timeline?df=version-0.0.14&y=ci|Check-ins derived from the 0.0.14 release],
     [/vdiff?from=version-0.0.14&to=trunk|content diff]
  *  [./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]
  *  [./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
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
1
2
3
4
5
6
7
8

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








-
+















+
+








<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
     put your file into the directory where your zettel are stored. Make sure
     place your file into the directory where your zettel are stored. Make sure
     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;

<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
     <tt>\]</tt>.
  *  &hellip;

<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.
  *  &hellip;