Zettelstore

Check-in Differences
Login

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

Difference From v0.0.15 To v0.1.2

2021-11-18
15:41
Typo in download page ... (check-in: 41b1f504ee user: stern tags: branch-0.1)
15:31
Version 0.1.2 ... (check-in: 8065f433c2 user: stern tags: release, branch-0.1, v0.1.2)
2021-11-13
17:09
Version 0.1.1 ... (check-in: 8d9dc27ae1 user: stern tags: trunk, release, v0.1.1)
2021-09-23
18:03
Increase version to 0.0.16-dev to begin next development cycle ... (check-in: 9748c52fdb user: stern tags: trunk)
2021-09-17
14:55
Version 0.0.15 ... (check-in: 66498fc30a user: stern tags: trunk, release, version-0.0.15, v0.0.15)
2021-09-16
18:02
Initial development zettel ... (check-in: 64e566f5db 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
1
2
3
4
5
6
7
8
9

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









-
+




+
+
+








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

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

check:
	go run tools/build.go check

relcheck:
	go run tools/build.go relcheck

api:
	go run tools/build.go testapi

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

build:

Changes to README.md.

8
9
10
11
12
13
14






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







+
+
+
+
+
+






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

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

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

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

[Stay tuned](https://twitter.com/zettelstore)…

Changes to VERSION.

1


1
-
+
0.0.15
0.1.2

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





















































































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

// ZidMetaJSON contains the identifier and the metadata of a zettel.
type ZidMetaJSON struct {
	ID   string            `json:"id"`
	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"`
	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"`
	Linked struct {
		Incoming []string `json:"incoming,omitempty"`
		Outgoing []string `json:"outgoing,omitempty"`
		Local    []string `json:"local,omitempty"`
		External []string `json:"external,omitempty"`
		Meta     []string `json:"meta,omitempty"`
	} `json:"linked"`
	Embedded struct {
		Outgoing []string `json:"outgoing,omitempty"`
		Local    []string `json:"local,omitempty"`
		External []string `json:"external,omitempty"`
	} `json:"embedded,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"`
	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 []ZidMetaJSON `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








































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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"
	QueryKeyEncoding = "_enc"
	QueryKeyLimit    = "_limit"
	QueryKeyNegate   = "_negate"
	QueryKeyOffset   = "_offset"
	QueryKeyOrder    = "_order"
	QueryKeyPart     = "_part"
	QueryKeySearch   = "_s"
	QueryKeySort     = "_sort"
)

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

// Supported encoding values.
const (
	EncodingDJSON  = "djson"
	EncodingHTML   = "html"
	EncodingNative = "native"
	EncodingText   = "text"
	EncodingZMK    = "zmk"
)

var mapEncodingEnum = map[string]EncodingEnum{
	EncodingDJSON:  EncoderDJSON,
	EncodingHTML:   EncoderHTML,
	EncodingNative: EncoderNative,
	EncodingText:   EncoderText,
	EncodingZMK:    EncoderZmk,
}
var mapEnumEncoding = map[EncodingEnum]string{}

func init() {
	for k, v := range mapEncodingEnum {
		mapEnumEncoding[v] = k
	}
}

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

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

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

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

// Supported part values.
const (
	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/inline.go.

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







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







}

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

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

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

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

Changes to ast/ref.go.

33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
33
34
35
36
37
38
39

40
41
42
43
44
45
46
47







-
+







		}
	}
	u, err := url.Parse(s)
	if err != nil {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil {
		if _, err := id.Parse(u.Path); err == nil {
		if _, err = id.Parse(u.Path); err == nil {
			return &Reference{URL: u, Value: s, State: RefStateZettel}
		}
		if u.Path == "" && u.Fragment != "" {
			return &Reference{URL: u, Value: s, State: RefStateSelf}
		}
	}
	return &Reference{URL: u, Value: s, State: RefStateExternal}

Changes to auth/impl/impl.go.

15
16
17
18
19
20
21

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







+







	"errors"
	"hash/fnv"
	"io"
	"time"

	"github.com/pascaldekloe/jwt"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
79
80
81
82
83
84
85
86

87
88
89

90
91
92
93
94
95
96
80
81
82
83
84
85
86

87
88
89

90
91
92
93
94
95
96
97







-
+


-
+







var ErrOtherKind = errors.New("auth: wrong token kind")

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

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

	now := time.Now().Round(time.Second)
	claims := jwt.Claims{
		Registered: jwt.Registered{
129
130
131
132
133
134
135
136
137


138
139
140
141
142
143
144
130
131
132
133
134
135
136


137
138
139
140
141
142
143
144
145







-
-
+
+







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

175
176
177
178
179
180
181
182
183
184
168
169
170
171
172
173
174

175
176
177
178
179
180
181
182
183
184
185







-
+










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

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

Changes to auth/policy/default.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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23



24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42

43
44
45
46
47

48
49

50
51

52
53
54
55
56







+








-
-
-
+
+
+






-
+









-
+




-
+

-
+

-
+




// under this license.
//-----------------------------------------------------------------------------

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

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

type defaultPolicy struct {
	manager auth.AuthzManager
}

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

func (d *defaultPolicy) canChange(user, m *meta.Meta) bool {
	metaRo, ok := m.Get(meta.KeyReadOnly)
	metaRo, ok := m.Get(api.KeyReadOnly)
	if !ok {
		return true
	}
	if user == nil {
		// If we are here, there is no authentication.
		// See owner.go:CanWrite.

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

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

Changes to auth/policy/owner.go.

8
9
10
11
12
13
14

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







+







// under this license.
//-----------------------------------------------------------------------------

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

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

type ownerPolicy struct {
	manager    auth.AuthzManager
30
31
32
33
34
35
36
37

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

38
39
40
41
42
43
44
45







-
+







	return o.userIsOwner(user) || o.userCanCreate(user, newMeta)
}

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

func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool {
	// No need to call o.pre.CanRead(user, meta), because it will always return true.
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81




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

99
100
101
102
103
104
105
57
58
59
60
61
62
63

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




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

99
100
101
102
103
104
105
106







-
+














-
-
-
-
+
+
+
+
















-
+







		return false
	case meta.VisibilityPublic:
		return true
	}
	if user == nil {
		return false
	}
	if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
	if role, ok := m.Get(api.KeyRole); ok && role == api.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
	case meta.UserRoleCreator:
		return vis == meta.VisibilityCreator
	default:
		return false
	}
}

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

func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) {
		return false
	}
	vis := o.authConfig.GetVisibility(oldMeta)
	if res, ok := o.checkVisibility(user, vis); ok {
		return res
	}
	if o.userIsOwner(user) {
		return true
	}
	if !o.userCanRead(user, oldMeta, vis) {
		return false
	}
	if role, ok := oldMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
	if role, ok := oldMeta.Get(api.KeyRole); ok && role == api.ValueRoleUser {
		// Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and
		// user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid)
		for _, key := range noChangeUser {
			if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") {
				return false
			}
		}
142
143
144
145
146
147
148
149

150
151
152
153
143
144
145
146
147
148
149

150
151
152
153
154







-
+




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

Changes to auth/policy/policy_test.go.

11
12
13
14
15
16
17

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







+







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

import (
	"fmt"
	"testing"

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

func TestPolicies(t *testing.T) {
	t.Parallel()
55
56
57
58
59
60
61
62
63
64



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

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


92
93
94
95
96
97
98
56
57
58
59
60
61
62



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

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


91
92
93
94
95
96
97
98
99







-
-
-
+
+
+













-
+











-
-
+
+







}

type testAuthzManager struct {
	readOnly bool
	withAuth bool
}

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

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

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

type authConfig struct{ expert bool }

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

func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility {
	if vis, ok := m.Get(meta.KeyVisibility); ok {
func (*authConfig) GetVisibility(m *meta.Meta) meta.Visibility {
	if vis, ok := m.Get(api.KeyVisibility); ok {
		return meta.GetVisibility(vis)
	}
	return meta.VisibilityLogin
}

func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly bool) {
	t.Helper()
247
248
249
250
251
252
253
254

255
256
257
258
259
260
261
248
249
250
251
252
253
254

255
256
257
258
259
260
261
262







-
+







	zettel := newZettel()
	publicZettel := newPublicZettel()
	loginZettel := newLoginZettel()
	ownerZettel := newOwnerZettel()
	expertZettel := newExpertZettel()
	userZettel := newUserZettel()
	writerNew := writer.Clone()
	writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, ""))
	writerNew.Set(api.KeyUserRole, owner.GetDefault(api.KeyUserRole, ""))
	roFalse := newRoFalseZettel()
	roTrue := newRoTrueZettel()
	roReader := newRoReaderZettel()
	roWriter := newRoWriterZettel()
	roOwner := newRoOwnerZettel()
	notAuthNotReadonly := !withAuth && !readonly
	testCases := []struct {
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
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







-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+


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

Changes to box/box.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"
	"errors"
	"fmt"
	"io"
	"time"

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







-
+





-
+




-
+







		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.GetDefault(api.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.GetDefault(api.KeyUserID, "?"),
		err.User.Zid.String())
}

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

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

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

Changes to box/compbox/compbox.go.

11
12
13
14
15
16
17

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







+







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

import (
	"context"
	"net/url"

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

34
35
36
37
38
39
40
41

42
43
44
45
46
47
48






49
50
51
52
53
54
55
35
36
37
38
39
40
41

42
43






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







-
+

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







	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
	content func(*meta.Meta) []byte
}{
	id.VersionZid:              {genVersionBuildM, genVersionBuildC},
	id.HostZid:                 {genVersionHostM, genVersionHostC},
	id.OperatingSystemZid:      {genVersionOSM, genVersionOSC},
	id.BoxManagerZid:           {genManagerM, genManagerC},
	id.MetadataKeyZid:          {genKeysM, genKeysC},
	id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC},
	id.MustParse(api.ZidVersion):              {genVersionBuildM, genVersionBuildC},
	id.MustParse(api.ZidHost):                 {genVersionHostM, genVersionHostC},
	id.MustParse(api.ZidOperatingSystem):      {genVersionOSM, genVersionOSC},
	id.MustParse(api.ZidBoxManager):           {genManagerM, genManagerC},
	id.MustParse(api.ZidMetadataKey):          {genKeysM, genKeysC},
	id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC},
}

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

143
144
145
146
147
148
149
150
151
152
153
154
155
156







157
158
144
145
146
147
148
149
150







151
152
153
154
155
156
157
158
159







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



func (*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)
	m.Set(api.KeyNoIndex, api.ValueTrue)
	m.Set(api.KeySyntax, api.ValueSyntaxZmk)
	m.Set(api.KeyRole, api.ValueRoleConfiguration)
	m.Set(api.KeyLang, api.ValueLangEN)
	m.Set(api.KeyReadOnly, api.ValueTrue)
	if _, ok := m.Get(api.KeyVisibility); !ok {
		m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	}
}

Changes to box/compbox/config.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
8
9
10
11
12
13
14

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


27
28
29
30
31


32
33
34
35

36
37



38
39
40
41

42
43
44

45
46

47
48

49
50
51

52
53







-
+

+









-
-
+
+



-
-
+
+


-
+

-
-
-
+
+
+

-
+


-
+

-
+

-
+


-
+

// under this license.
//-----------------------------------------------------------------------------

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

import (
	"strings"
	"bytes"

	"zettelstore.de/c/api"
	"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)
	m.Set(api.KeyTitle, "Zettelstore Startup Configuration")
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m
}

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

Changes to box/compbox/keys.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
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24


25
26
27
28
29

30
31


32
33
34

35
36
37

38
39







+

-

+






-
-
+
+



-
+

-
-
+
+

-
+


-
+

// under this license.
//-----------------------------------------------------------------------------

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

import (
	"bytes"
	"fmt"
	"strings"

	"zettelstore.de/c/api"
	"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)
	m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys")
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}

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

Changes to box/compbox/manager.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
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25

26
27
28
29

30
31
32

33
34


35
36
37

38
39

40
41







+

-

+







-
+



-
+


-
+

-
-
+
+

-
+

-
+

// under this license.
//-----------------------------------------------------------------------------

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

import (
	"bytes"
	"fmt"
	"strings"

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

func genManagerM(zid id.Zid) *meta.Meta {
	m := meta.New(zid)
	m.Set(meta.KeyTitle, "Zettelstore Box Manager")
	m.Set(api.KeyTitle, "Zettelstore Box Manager")
	return m
}

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

Changes to box/compbox/version.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
8
9
10
11
12
13
14


15
16
17
18
19
20
21
22


23
24
25
26
27
28
29

30
31
32


33
34
35
36
37
38
39


40
41
42
43
44
45
46

47




48
49

50
51
52
53
54







-
-
+







-
-
+
+





-
+


-
-
+
+





-
-
+
+





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

// under this license.
//-----------------------------------------------------------------------------

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

import (
	"fmt"

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

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

func genVersionBuildM(zid id.Zid) *meta.Meta {
	m := getVersionMeta(zid, "Zettelstore Version")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
	m.Set(api.KeyVisibility, api.ValueVisibilityPublic)
	return m
}
func genVersionBuildC(*meta.Meta) string {
	return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)
func genVersionBuildC(*meta.Meta) []byte {
	return []byte(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 genVersionHostC(*meta.Meta) []byte {
	return []byte(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 {
func genVersionOSC(*meta.Meta) []byte {
	return fmt.Sprintf(
		"%v/%v",
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string),
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string),
	goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string)
	goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string)
	)
	result := make([]byte, 0, len(goOS)+len(goArch)+1)
	result = append(result, goOS...)
	result = append(result, '/')
	return append(result, goArch...)
}

Changes to box/constbox/base.css.

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

13

14
15
16
17
18
19
20












-

-







*,*::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;
229
230
231
232
233
234
235








236
237
238
239
240











241
242
243
244
245
246
247
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







+
+
+
+
+
+
+
+





+
+
+
+
+
+
+
+
+
+
+







  span.zs-indication {
    border: 1px solid black;
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 95%;
  }
  .zs-example { border-style: dotted !important }
  .zs-info {
    background-color: lightblue;
    padding: .5rem 1rem;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5rem 1rem;
  }
  .zs-error {
    background-color: lightpink;
    border-style: none !important;
    font-weight: bold;
  }
  .zs-ta-left { text-align:left }
  .zs-ta-center { text-align:center }
  .zs-ta-right { text-align:right }
  .zs-monospace { font-family:monospace }
  .zs-font-size-0 { font-size:75% }
  .zs-font-size-1 { font-size:83% }
  .zs-font-size-2 { font-size:100% }
  .zs-font-size-3 { font-size:117% }
  .zs-font-size-4 { font-size:150% }
  .zs-font-size-5 { font-size:200% }
  .zs-deprecated { border-style: dashed; padding: .2rem }
  kbd {
    background: hsl(210, 5%, 100%);
    border: 1px solid hsl(210, 5%, 70%);
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 75%;
  }

Changes to box/constbox/base.mustache.

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



1

2
3
4
5
6
7
8
-
-
-

-







<!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>
58
59
60
61
62
63
64
65
66
54
55
56
57
58
59
60









-
-
{{{Content}}}
</main>
{{#FooterHTML}}
<footer>
{{{FooterHTML}}}
</footer>
{{/FooterHTML}}
</body>
</html>

Changes to box/constbox/constbox.go.

12
13
14
15
16
17
18

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







+







package constbox

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

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

116
117
118
119
120
121
122
123
124
125
126
127





128
129
130


131
132
133
134
135
136
137






138
139
140

141
142
143
144
145
146
147






148
149
150

151
152
153
154
155
156
157






158
159
160
161
162
163
164
165
166





167
168
169
170
171
172
173
174
175





176
177
178
179
180
181
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

117
118
119
120
121
122
123





124
125
126
127
128
129


130
131
132






133
134
135
136
137
138
139
140

141
142






143
144
145
146
147
148
149
150

151
152






153
154
155
156
157
158
159
160
161
162





163
164
165
166
167
168
169
170
171





172
173
174
175
176
177
178
179
180





181
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







-
-
-
-
-
+
+
+
+
+

-
-
+
+

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


-
+

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


-
+

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




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+


-
+

-
-
-
-
-
+
+
+
+
+


-
+

-
-
-
-
+
+
+
+

-
+


-
-
-
-
-
+
+
+
+
+




-
-
-
-
-
+
+
+
+
+


-
+

-
-
-
-
+
+
+
+

-
-
+
+

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

-
+


-
-
-
-
+
+
+
+





-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+


-
+
}

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,
			api.KeyTitle:      "Zettelstore Runtime Configuration",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxNone,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityOwner,
		},
		domain.NewContent("")},
	id.LicenseZid: {
		domain.NewContent(nil)},
	id.MustParse(api.ZidLicense): {
		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,
			api.KeyTitle:      "Zettelstore License",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxText,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent(contentLicense)},
	id.AuthorsZid: {
	id.MustParse(api.ZidAuthors): {
		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,
			api.KeyTitle:      "Zettelstore Contributors",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent(contentContributors)},
	id.DependenciesZid: {
	id.MustParse(api.ZidDependencies): {
		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,
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Info HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Context HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Rename Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore List Roles HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore List Tags HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "Zettelstore Error HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentErrorMustache)},
	id.BaseCSSZid: {
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     "css",
			meta.KeyNoIndex:    meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     "css",
			api.KeyNoIndex:    api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent(contentBaseCSS)},
	id.UserCSSZid: {
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			meta.KeyTitle:      "Zettelstore User CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     "css",
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     "css",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent("/* User-defined CSS */")},
		domain.NewContent([]byte("/* 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,
			api.KeyTitle:      "Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.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,
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		domain.NewContent(contentNewTOCZettel)},
	id.TemplateNewZettelZid: {
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			meta.KeyTitle:      "New Zettel",
			meta.KeyRole:       meta.ValueRoleZettel,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyVisibility: meta.ValueVisibilityCreator,
			api.KeyTitle:      "New Zettel",
			api.KeyRole:       api.ValueRoleZettel,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		domain.NewContent("")},
	id.TemplateNewUserZid: {
		domain.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewUser): {
		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,
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.ValueRoleUser,
			api.KeySyntax:                      api.ValueSyntaxNone,
			meta.NewPrefix + api.KeyCredential: "",
			meta.NewPrefix + api.KeyUserID:     "",
			meta.NewPrefix + api.KeyUserRole:   api.ValueUserRoleReader,
			api.KeyVisibility:                  api.ValueVisibilityOwner,
		},
		domain.NewContent("")},
		domain.NewContent(nil)},
	id.DefaultHomeZid: {
		constHeader{
			meta.KeyTitle:  "Home",
			meta.KeyRole:   meta.ValueRoleZettel,
			meta.KeySyntax: meta.ValueSyntaxZmk,
			meta.KeyLang:   meta.ValueLangEN,
			api.KeyTitle:  "Home",
			api.KeyRole:   api.ValueRoleZettel,
			api.KeySyntax: api.ValueSyntaxZmk,
			api.KeyLang:   api.ValueLangEN,
		},
		domain.NewContent(contentHomeZettel)},
}

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

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

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

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

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

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

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

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

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

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

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

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

//go:embed listroles.mustache
var contentListRolesMustache string
var contentListRolesMustache []byte

//go:embed listtags.mustache
var contentListTagsMustache string
var contentListTagsMustache []byte

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

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

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

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

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

Changes to box/constbox/delete.mustache.

1
2
3
4
5

















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





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










<article>
<header>
<h1>Delete Zettel {{Zid}}</h1>
</header>
<p>Do you really want to delete this zettel?</p>
{{#HasShadows}}
<div class="zs-info">
<h2>Infomation</h2>
<p>If you delete this zettel, the previoulsy shadowed zettel from overlayed box {{ShadowedBox}} becomes available.</p>
</div>
{{/HasShadows}}
{{#HasIncoming}}
<div class="zs-warning">
<h2>Warning!</h2>
<p>If you delete this zettel, incoming references from the following zettel will become invalid.</p>
<ul>
{{#Incoming}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Incoming}}
</ul>
</div>
{{/HasIncoming}}
<dl>
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
<form method="POST">
<input class="zs-button" type="submit" value="Delete">
</form>
</article>
{{end}}

Changes to box/constbox/listtags.mustache.

1
2
3
4
5
6
7
8

9
10
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}} <a href="{{{URL}}}" class="zs-font-size-{{Size}}">{{Name}}</a><sup>{{Count}}</sup>
{{/Tags}}
</nav>

Changes to box/constbox/rename.mustache.

1
2
3
4
5











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





+
+
+
+
+
+
+
+
+
+
+







<article>
<header>
<h1>Rename Zettel {{.Zid}}</h1>
</header>
<p>Do you really want to rename this zettel?</p>
{{#HasIncoming}}
<div class="zs-warning">
<h2>Warning!</h2>
<p>If you rename this zettel, incoming references from the following zettel will become invalid.</p>
<ul>
{{#Incoming}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Incoming}}
</ul>
</div>
{{/HasIncoming}}
<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">

Changes to box/dirbox/dirbox.go.

18
19
20
21
22
23
24

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







+







	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"zettelstore.de/c/api"
	"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"
292
293
294
295
296
297
298
299

300
301

302
303
304
305
306
307
308
293
294
295
296
297
298
299

300
301

302
303
304
305
306
307
308
309







-
+

-
+







	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")
	syntax := m.GetDefault(api.KeySyntax, "bin")
	switch syntax {
	case meta.ValueSyntaxNone, meta.ValueSyntaxZmk:
	case api.ValueSyntaxNone, api.ValueSyntaxZmk:
		return directory.MetaSpecHeader, "zettel"
	}
	for _, s := range dp.cdata.Config.GetZettelFileSyntax() {
		if s == syntax {
			return directory.MetaSpecHeader, "zettel"
		}
	}
388
389
390
391
392
393
394
395
396


397
398
399


400
401
402
403
404
405
406
407
408
409
410
389
390
391
392
393
394
395


396
397
398


399
400
401
402
403
404
405
406
407
408
409
410
411







-
-
+
+

-
-
+
+












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

func (dp *dirBox) cleanupMeta(m *meta.Meta) {
	if role, ok := m.Get(meta.KeyRole); !ok || role == "" {
		m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole())
	if role, ok := m.Get(api.KeyRole); !ok || role == "" {
		m.Set(api.KeyRole, dp.cdata.Config.GetDefaultRole())
	}
	if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax())
	if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
		m.Set(api.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
}

Changes to box/dirbox/service.go.

71
72
73
74
75
76
77
78

79
80
81
82
83
84
85
86
87
88
89
90
91
92

93
94
95
96
97
98

99
100
101
102
103
104
105
71
72
73
74
75
76
77

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

92
93
94
95
96
97

98
99
100
101
102
103
104
105







-
+













-
+





-
+







	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) {
func getMetaContent(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, []byte, 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
	content []byte
	err     error
}

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

	entry := cmd.entry
	switch entry.MetaSpec {
	case directory.MetaSpecFile:
		m, err = parseMetaFile(entry.Zid, entry.MetaPath)
		if err == nil {
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
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







-
-
+
+
-
-
-
-











-
+


-
+







		panic("TODO: ???")
	}
	cmd.rc <- err
}

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

func readFileContent(path string) (string, error) {
	data, err := os.ReadFile(path)
func readFileContent(path string) ([]byte, error) {
	return 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) {
func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, []byte, error) {
	src, err := readFileContent(path)
	if err != nil {
		return nil, "", err
		return nil, nil, err
	}
	inp := input.NewInput(src)
	meta := meta.NewFromInput(zid, inp)
	return meta, src[inp.Pos:], nil
}

func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) {

Changes to box/dirbox/simpledir/simpledir.go.

38
39
40
41
42
43
44
45

46
47
48
49
50
51
52

53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71

72

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91


92
93
94
95
96
97
98
38
39
40
41
42
43
44

45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61

62
63
64
65
66
67
68
69
70
71
72

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


91
92
93
94
95
96
97
98
99







-
+






-
+









-
+









+
-
+

















-
-
+
+







func (ss *simpleService) Start() error {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	_, err := os.ReadDir(ss.dirPath)
	return err
}

func (ss *simpleService) Stop() error {
func (*simpleService) Stop() error {
	return nil
}

func (ss *simpleService) NumEntries() (int, error) {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	entries, err := ss.getEntries()
	entries, err := ss.doGetEntries()
	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()
	entrySet, err := ss.doGetEntries()
	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) {
func (ss *simpleService) doGetEntries() (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 {
		zid, err2 := id.Parse(match[1])
		if err2 != nil {
			continue
		}
		var entry *directory.Entry
		if e, ok := entrySet[zid]; ok {
			entry = e
		} else {
			entry = &directory.Entry{Zid: zid}
127
128
129
130
131
132
133
134

135

136

137
138
139
140
141
142
143
128
129
130
131
132
133
134

135
136
137

138
139
140
141
142
143
144
145







-
+

+
-
+







		entry.ContentExt = ext
	}
}

func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) {
	ss.mx.Lock()
	defer ss.mx.Unlock()
	return ss.getEntry(zid)
	return ss.doGetEntry(zid)
}

func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) {
func (ss *simpleService) doGetEntry(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
153
154
155
156
157
158
159
160

161
162
163
164
165
166
167
168
169
170
171
172

173
174
175
176
177

178
179
180
181
182

183
184
185
155
156
157
158
159
160
161

162
163
164
165
166
167
168
169
170
171
172
173

174
175
176
177
178

179
180
181
182
183

184
185
186
187







-
+











-
+




-
+




-
+



	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)
		entry, err := ss.doGetEntry(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 {
func (*simpleService) UpdateEntry(*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 {
func (*simpleService) RenameEntry(_, _ *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 {
func (*simpleService) DeleteEntry(id.Zid) error {
	// Nothing to to, since the actual file delete is done by dirbox.
	return nil
}

Changes to box/filebox/filebox.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 (
	"errors"
	"net/url"
	"path/filepath"
	"strings"

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

func init() {
62
63
64
65
66
67
68
69
70


71
72
73
74
75
76
77


78
79
80
81

82
83

84
85
86
87

88
89
90
91
92

93
94
63
64
65
66
67
68
69


70
71
72
73
74
75
76


77
78
79
80
81

82
83

84
85
86
87

88
89
90
91
92

93
94
95







-
-
+
+





-
-
+
+



-
+

-
+



-
+




-
+


	}
	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))
	m.Set(api.KeyTitle, zid.String())
	m.Set(api.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 title, ok := m.Get(api.KeyTitle); !ok || title == "" {
		m.Set(api.KeyTitle, zid.String())
	}

	if inMeta {
		if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
			dm := CalcDefaultMeta(zid, ext)
			syntax, ok = dm.Get(meta.KeySyntax)
			syntax, ok = dm.Get(api.KeySyntax)
			if !ok {
				panic("Default meta must contain syntax")
			}
			m.Set(meta.KeySyntax, syntax)
			m.Set(api.KeySyntax, syntax)
		}
	}

	if duplicates {
		m.Set(meta.KeyDuplicates, meta.ValueTrue)
		m.Set(api.KeyDuplicates, api.ValueTrue)
	}
}

Changes to box/filebox/zipbox.go.

60
61
62
63
64
65
66
67
68


69
70
71
72
73
74
75
60
61
62
63
64
65
66


67
68
69
70
71
72
73
74
75







-
-
+
+







	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 {
		zid, err2 := id.Parse(match[1])
		if err2 != nil {
			continue
		}
		zp.addFile(zid, f.Name, match[3])
	}
	return nil
}

116
117
118
119
120
121
122
123

124
125
126
127
128
129
130
116
117
118
119
120
121
122

123
124
125
126
127
128
129
130







-
+







	reader, err := zip.OpenReader(zp.name)
	if err != nil {
		return domain.Zettel{}, err
	}
	defer reader.Close()

	var m *meta.Meta
	var src string
	var src []byte
	var inMeta bool
	if entry.metaInHeader {
		src, err = readZipFileContent(reader, entry.contentName)
		if err != nil {
			return domain.Zettel{}, err
		}
		inp := input.NewInput(src)
170
171
172
173
174
175
176
177
178


179
180
181
182
183
184
185
170
171
172
173
174
175
176


177
178
179
180
181
182
183
184
185







-
-
+
+







func (zp *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc) error {
	reader, err := zip.OpenReader(zp.name)
	if err != nil {
		return err
	}
	defer reader.Close()
	for zid, entry := range zp.zettel {
		m, err := readZipMeta(reader, zid, entry)
		if err != nil {
		m, err2 := readZipMeta(reader, zid, entry)
		if err2 != nil {
			continue
		}
		zp.enricher.Enrich(ctx, m, zp.number)
		handle(m)
	}
	return nil
}
235
236
237
238
239
240
241
242

243
244
245

246
247
248

249
250
251

252
253
235
236
237
238
239
240
241

242
243
244

245
246
247

248



249









-
+


-
+


-
+
-
-
-
+
-
-
	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) {
func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) {
	f, err := reader.Open(name)
	if err != nil {
		return "", err
		return nil, err
	}
	defer f.Close()
	buf, err := io.ReadAll(f)
	return io.ReadAll(f)
	if err != nil {
		return "", err
	}
}
	return string(buf), nil
}

Changes to box/manager/anteroom_test.go.

37
38
39
40
41
42
43
44

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

44
45
46
47
48
49
50
51







-
+







	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()
		action, _, _ = ar.Dequeue()
		if action == arNothing {
			break
		}
	}
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}

Changes to box/manager/box.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
8
9
10
11
12
13
14
15
16
17

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

33
34
35

36
37

38
39

40
41
42
43
44
45
46
47







+


-















-
+


-
+

-
+

-
+







// under this license.
//-----------------------------------------------------------------------------

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

import (
	"bytes"
	"context"
	"errors"
	"strings"

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

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

Changes to box/manager/enrich.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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

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

import (
	"context"
	"strconv"

	"zettelstore.de/c/api"
	"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))
	m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber))
	computePublished(m)
	mgr.idxStore.Enrich(ctx, m)
}

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

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

Changes to box/manager/indexer.go.

12
13
14
15
16
17
18

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







+







package manager

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

	"zettelstore.de/c/api"
	"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"
139
140
141
142
143
144
145
146

147
148
149
150
151
152
153
140
141
142
143
144
145
146

147
148
149
150
151
152
153
154







-
+







		return false
	}
	return true
}

func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) {
	m := zettel.Meta
	if m.GetBool(meta.KeyNoIndex) {
	if m.GetBool(api.KeyNoIndex) {
		// Zettel maybe in index
		toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid)
		mgr.idxCheckZettel(toCheck)
		return
	}

	var cData collectData
201
202
203
204
205
206
207
208

209
210
211
212
213
214
215
202
203
204
205
206
207
208

209
210
211
212
213
214
215
216







-
+







}

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

Changes to box/manager/manager.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 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"
69
70
71
72
73
74
75
76

77
78
79
80
81
82
83
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82







-
+







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)
		panic(scheme)
	}
	registry[scheme] = create
}

// GetSchemes returns all registered scheme, ordered by scheme string.
func GetSchemes() []string {
	result := make([]string, 0, len(registry))
231
232
233
234
235
236
237
238

239
240
241
242
243
244
245
230
231
232
233
234
235
236

237
238
239
240
241
242
243
244







-
+







			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 {
			if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 {
				ssj.Stop(ctx)
			}
		}
		mgr.mgrMx.Unlock()
		return err
	}
	mgr.idxAr.Reset() // Ensure an initial index run

Changes to box/manager/memstore/memstore.go.

15
16
17
18
19
20
21

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







+







	"context"
	"fmt"
	"io"
	"sort"
	"strings"
	"sync"

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

type metaRefs struct {
	forward  id.Slice
82
83
84
85
86
87
88
89

90
91
92
93
94

95
96
97
98

99
100
101
102
103
104
105
106
107





108
109
110
111
112

113
114
115
116
117




118
119

120
121
122
123


124
125
126
127
128
129
130
83
84
85
86
87
88
89

90
91
92
93
94

95
96
97
98

99
100
101
102






103
104
105
106
107

108
109
110

111
112
113



114
115
116
117
118

119
120
121


122
123
124
125
126
127
128
129
130







-
+




-
+



-
+



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



-
+


-
-
-
+
+
+
+

-
+


-
-
+
+







	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())
		m.Set(api.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Copy())
	if len(zi.backward) > 0 {
		m.Set(meta.KeyBackward, zi.backward.String())
		m.Set(api.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(meta.KeyForward, zi.forward.String())
		m.Set(api.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
	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())
		m.Set(api.KeyBack, back.String())
		updated = true
	}
	if zi.itags != "" {
		if tags, ok := m.Get(meta.KeyTags); ok {
			m.Set(meta.KeyAllTags, tags+" "+zi.itags)
	if itags := zi.itags; itags != "" {
		m.Set(api.KeyContentTags, itags)
		if tags, ok2 := m.Get(api.KeyTags); ok2 {
			m.Set(api.KeyAllTags, tags+" "+itags)
		} else {
			m.Set(meta.KeyAllTags, zi.itags)
			m.Set(api.KeyAllTags, itags)
		}
		updated = true
	} else if tags, ok := m.Get(meta.KeyTags); ok {
		m.Set(meta.KeyAllTags, tags)
	} else if tags, ok2 := m.Get(api.KeyTags); ok2 {
		m.Set(api.KeyAllTags, tags)
		updated = true
	}
	return updated
}

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.

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



























































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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"
	"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
	client    http.Client
}

// NewClient create a new client.
func NewClient(baseURL string) *Client {
	if !strings.HasSuffix(baseURL, "/") {
		baseURL += "/"
	}
	c := Client{
		baseURL: baseURL,
		client: http.Client{
			Timeout: 10 * time.Second,
			Transport: &http.Transport{
				DialContext: (&net.Dialer{
					Timeout: 5 * time.Second, // TCP connect timeout
				}).DialContext,
				TLSHandshakeTimeout: 5 * time.Second,
			},
		},
	}
	return &c
}

func (c *Client) newURLBuilder(key byte) *api.URLBuilder {
	return api.NewURLBuilder(c.baseURL, key)
}
func (*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)
	}
	resp, err := c.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('a'), 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('a'), 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 string) (id.Zid, error) {
	ub := c.jsonZettelURLBuilder('z', nil)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, strings.NewReader(data), nil)
	if err != nil {
		return id.Invalid, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusCreated {
		return id.Invalid, errors.New(resp.Status)
	}
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return id.Invalid, err
	}
	zid, err := id.Parse(string(b))
	if err != nil {
		return id.Invalid, err
	}
	return zid, nil
}

// CreateZettelJSON creates a new zettel and returns its URL.
func (c *Client) CreateZettelJSON(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('j', 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) (string, error) {
	ub := c.jsonZettelURLBuilder('z', query)
	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)
	}
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// ListZettelJSON returns a list of all Zettel.
func (c *Client) ListZettelJSON(ctx context.Context, query url.Values) ([]api.ZidMetaJSON, error) {
	ub := c.jsonZettelURLBuilder('j', 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
}

// GetZettel returns a zettel as a string.
func (c *Client) GetZettel(ctx context.Context, zid id.Zid, part string) (string, error) {
	ub := c.jsonZettelURLBuilder('z', nil).SetZid(zid)
	if part != "" && part != api.PartContent {
		ub.AppendQuery(api.QueryKeyPart, part)
	}
	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)
	}
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// GetZettelJSON returns a zettel as a JSON struct.
func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid) (*api.ZettelDataJSON, error) {
	ub := c.jsonZettelURLBuilder('j', nil).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
}

// GetParsedZettel return a parsed zettel in a defined encoding.
func (c *Client) GetParsedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) {
	return c.getZettelString(ctx, 'p', zid, enc)
}

// GetEvaluatedZettel return an evaluated zettel in a defined encoding.
func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) {
	return c.getZettelString(ctx, 'v', zid, enc)
}

func (c *Client) getZettelString(ctx context.Context, key byte, zid id.Zid, enc api.EncodingEnum) (string, error) {
	ub := c.jsonZettelURLBuilder(key, nil).SetZid(zid)
	ub.AppendQuery(api.QueryKeyEncoding, 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 other zettel, embedded material, 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 string) error {
	ub := c.jsonZettelURLBuilder('z', nil).SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, strings.NewReader(data), nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNoContent {
		return errors.New(resp.Status)
	}
	return nil
}

// UpdateZettelJSON updates an existing zettel.
func (c *Client) UpdateZettelJSON(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('j', 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('z', nil).SetZid(oldZid)
	h := http.Header{
		api.HeaderDestination: {c.jsonZettelURLBuilder('z', 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('z', 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(key byte, query url.Values) *api.URLBuilder {
	ub := c.newURLBuilder(key)
	for key, values := range query {
		if key == api.QueryKeyEncoding {
			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
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






























































































































































































































































































































































































































































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

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

func TestCreateGetRenameDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	zettel := `title: A Test

Example content.`
	c := getClient()
	c.SetAuth("owner", "owner")
	zid, err := c.CreateZettel(context.Background(), zettel)
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	if !zid.IsValid() {
		t.Error("Invalid zettel ID", zid)
		return
	}
	data, err := c.GetZettel(context.Background(), zid, api.PartZettel)
	if err != nil {
		t.Error("Cannot read zettel", zid, err)
		return
	}
	exp := `title: A Test
role: zettel
syntax: zmk

Example content.`
	if data != exp {
		t.Errorf("Expected zettel data: %q, but got %q", exp, data)
	}
	newZid := zid + 1
	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 TestCreateRenameDeleteZettelJSON(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("creator", "creator")
	zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{
		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("owner", "owner")
	z, err := c.GetZettel(context.Background(), id.DefaultHomeZid, api.PartZettel)
	if err != nil {
		t.Error(err)
		return
	}
	if !strings.HasPrefix(z, "title: Home\n") {
		t.Error("Got unexpected zettel", z)
		return
	}
	newZettel := `title: New Home
role: zettel
syntax: zmk

Empty`
	err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, newZettel)
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettel(context.Background(), id.DefaultHomeZid, api.PartZettel)
	if err != nil {
		t.Error(err)
		return
	}
	if zt != newZettel {
		t.Errorf("Expected zettel %q, got %q", newZettel, zt)
	}
	// Must delete to clean up for next tests
	err = c.DeleteZettel(context.Background(), id.DefaultHomeZid)
	if err != nil {
		t.Error("Cannot delete", id.DefaultHomeZid, ":", err)
		return
	}
}

func TestUpdateZettelJSON(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("writer", "writer")
	z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid)
	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.UpdateZettelJSON(context.Background(), id.DefaultHomeZid, z)
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid)
	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)
	}
	// No need to clean up, because we just changed the title.
}

func TestListZettel(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.QueryKeyEncoding: {api.EncodingHTML}} // 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.ListZettelJSON(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.ListZettelJSON(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)
	}

	pl, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}})
	if err != nil {
		t.Error(err)
		return
	}
	lines := strings.Split(pl, "\n")
	if lines[len(lines)-1] == "" {
		lines = lines[:len(lines)-1]
	}
	if len(lines) != len(l) {
		t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(lines), len(l))
	} else {
		for i, line := range lines {
			if got := line[:14]; got != l[i].ID {
				t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got)
			}
		}
	}
}

func TestGetZettelJSON(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid)
	if err != nil {
		t.Error(err)
		return
	}
	if m := z.Meta; len(m) == 0 {
		t.Errorf("Exptected non-empty meta, but got %v", z.Meta)
	}
	if z.Content == "" || z.Encoding != "" {
		t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding)
	}
}

func TestGetParsedEvaluatedZettel(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.GetParsedZettel(context.Background(), id.DefaultHomeZid, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for parsed encoding %v", enc)
		}
		content, err = c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for evaluated encoding %v", enc)
		}
	}
}

func checkZid(t *testing.T, expected id.Zid, got string) bool {
	t.Helper()
	if exp := expected.String(); exp != got {
		t.Errorf("Expected a Zid %q, but got %q", exp, got)
		return false
	}
	return true
}

func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected id.Zid) {
	t.Helper()
	exp := expected.String()
	if got := l[pos].ID; got != exp {
		t.Errorf("Expected result[%d]=%v, but got %v", pos, exp, got)
	}
}

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 !checkZid(t, 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
	}
	checkListZid(t, l, 0, id.TemplateNewZettelZid)
	checkListZid(t, l, 1, id.TemplateNewUserZid)
}

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 !checkZid(t, 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
	}
	checkListZid(t, l, 0, id.DefaultHomeZid)
	checkListZid(t, l, 1, id.OperatingSystemZid)
	checkListZid(t, l, 2, id.StartupConfigurationZid)

	rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, 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
	}
	checkListZid(t, l, 0, id.DefaultHomeZid)
}

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 !checkZid(t, id.DefaultHomeZid, zl.ID) {
		return
	}
	if len(zl.Linked.Incoming) != 0 {
		t.Error("No incomings expected", zl.Linked.Incoming)
	}
	if got := len(zl.Linked.Outgoing); got != 4 {
		t.Errorf("Expected 4 outgoing links, got %d", got)
	}
	if got := len(zl.Linked.Local); got != 1 {
		t.Errorf("Expected 1 local link, got %d", got)
	}
	if got := len(zl.Linked.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
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41

42
43
44
45

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







-
+










-
+










-
+



-
+




















-
+








-
+



-
+



-
+




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

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

// ---------- Subcommand: file -----------------------------------------------

func cmdFile(fs *flag.FlagSet, _ *meta.Meta) (int, error) {
func cmdFile(fs *flag.FlagSet) (int, error) {
	enc := fs.Lookup("t").Value.String()
	m, inp, err := getInput(fs.Args())
	if m == nil {
		return 2, err
	}
	z := parser.ParseZettel(
		domain.Zettel{
			Meta:    m,
			Content: domain.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk),
		m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc), &encoder.Environment{
		Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN),
		Lang: m.GetDefault(api.KeyLang, api.ValueLangEN),
	})
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}
	_, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata)
	if err != nil {
		return 2, err
	}
	fmt.Println()

	return 0, nil
}

func getInput(args []string) (*meta.Meta, *input.Input, error) {
	if len(args) < 1 {
		src, err := io.ReadAll(os.Stdin)
		if err != nil {
			return nil, nil, err
		}
		inp := input.NewInput(string(src))
		inp := input.NewInput(src)
		m := meta.NewFromInput(id.New(true), inp)
		return m, inp, nil
	}

	src, err := os.ReadFile(args[0])
	if err != nil {
		return nil, nil, err
	}
	inp := input.NewInput(string(src))
	inp := input.NewInput(src)
	m := meta.NewFromInput(id.New(true), inp)

	if len(args) > 1 {
		src, err := os.ReadFile(args[1])
		src, err = os.ReadFile(args[1])
		if err != nil {
			return nil, nil, err
		}
		inp = input.NewInput(string(src))
		inp = input.NewInput(src)
	}
	return m, inp, nil
}

Changes to cmd/cmd_password.go.

1
2

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



23
24
25
26
27

28
29
30
31
32
33
34
1

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



20
21
22
23
24
25
26

27
28
29
30
31
32
33
34

-
+

















-
-
-
+
+
+




-
+







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

package cmd

import (
	"flag"
	"fmt"
	"os"

	"golang.org/x/term"

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

// ---------- Subcommand: password -------------------------------------------

func cmdPassword(fs *flag.FlagSet, cfg *meta.Meta) (int, error) {
func cmdPassword(fs *flag.FlagSet) (int, error) {
	if fs.NArg() == 0 {
		fmt.Fprintln(os.Stderr, "User name and user zettel identification missing")
		return 2, nil
	}
	if fs.NArg() == 1 {
		fmt.Fprintln(os.Stderr, "User zettel identification missing")
		return 2, nil
56
57
58
59
60
61
62
63
64


65
66
67
68
69
70
71
72
73
74
56
57
58
59
60
61
62


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







-
-
+
+











	ident := fs.Arg(0)
	hashedPassword, err := cred.HashCredential(zid, ident, password)
	if err != nil {
		return 2, err
	}
	fmt.Printf("%v: %s\n%v: %s\n",
		meta.KeyCredential, hashedPassword,
		meta.KeyUserID, ident,
		api.KeyCredential, hashedPassword,
		api.KeyUserID, ident,
	)
	return 0, nil
}

func getPassword(prompt string) (string, error) {
	fmt.Fprintf(os.Stderr, "%s: ", prompt)
	password, err := term.ReadPassword(int(os.Stdin.Fd()))
	fmt.Fprintln(os.Stderr)
	return string(password), err
}

Changes to cmd/cmd_run.go.

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

40
41
42
43
44
45

46
47
48
49
50

51
52
53

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83


84
85
86
87
88


89
90
91
92
93
94
95
12
13
14
15
16
17
18

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

38






39
40
41
42
43

44



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73
74

75
76
77
78
79
80

81
82
83
84
85
86
87
88
89







-



















-
+
-
-
-
-
-
-
+




-
+
-
-
-
+




















-
+








-
+
+




-
+
+








import (
	"flag"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter/api"
	"zettelstore.de/z/web/adapter/webui"
	"zettelstore.de/z/web/server"
)

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

func flgRun(fs *flag.FlagSet) {
	fs.String("c", defConfigfile, "configuration file")
	fs.Uint("a", 0, "port number kernel service (0=disable)")
	fs.Uint("p", 23123, "port number web service")
	fs.String("d", "", "zettel directory")
	fs.Bool("r", false, "system-wide read-only mode")
	fs.Bool("v", false, "verbose mode")
	fs.Bool("debug", false, "debug mode")
}

func withDebug(fs *flag.FlagSet) bool {
func runFunc(*flag.FlagSet) (int, error) {
	dbg := fs.Lookup("debug")
	return dbg != nil && dbg.Value.String() == "true"
}

func runFunc(fs *flag.FlagSet, _ *meta.Meta) (int, error) {
	exitCode, err := doRun(withDebug(fs))
	exitCode, err := doRun()
	kernel.Main.WaitForShutdown()
	return exitCode, err
}

func doRun(debug bool) (int, error) {
func doRun() (int, error) {
	kern := kernel.Main
	kern.SetDebug(debug)
	if err := kern.StartService(kernel.WebService); err != nil {
	if err := kernel.Main.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)
	a := api.New(webSrv, authManager, authManager, webSrv, rtConfig)
	wui := webui.New(webSrv, authManager, rtConfig, authManager, boxManager, authPolicy)

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

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

	// Web user interface
	if !authManager.IsReadonly() {
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta))
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(
			ucGetMeta, &ucEvaluate))
		webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(ucRename))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCopyZettelHandler(
			ucGetZettel, usecase.NewCopyZettel()))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(
			ucGetMeta, ucGetAllMeta, &ucEvaluate))
		webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(ucDelete))
		webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(ucUpdate))
		webSrv.AddZettelRoute('f', server.MethodGet, wui.MakeGetFolgeZettelHandler(
			ucGetZettel, usecase.NewFolgeZettel(rtConfig)))
		webSrv.AddZettelRoute('f', server.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
		webSrv.AddZettelRoute('g', server.MethodGet, wui.MakeGetNewZettelHandler(
112
113
114
115
116
117
118

119
120
121
122
123

124
125
126
127
128
129
130
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126







+





+








	// API
	webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(ucAuthenticate))
	webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
	webSrv.AddListRoute('j', server.MethodGet, api.MakeListMetaHandler(ucListMeta))
	webSrv.AddZettelRoute('j', server.MethodGet, api.MakeGetZettelHandler(ucGetZettel))
	webSrv.AddZettelRoute('l', server.MethodGet, api.MakeGetLinksHandler(ucEvaluate))
	webSrv.AddZettelRoute('m', server.MethodGet, api.MakeGetMetaHandler(ucGetMeta))
	webSrv.AddZettelRoute('o', server.MethodGet, api.MakeGetOrderHandler(
		usecase.NewZettelOrder(protectedBoxManager, ucEvaluate)))
	webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel))
	webSrv.AddListRoute('r', server.MethodGet, api.MakeListRoleHandler(ucListRoles))
	webSrv.AddListRoute('t', server.MethodGet, api.MakeListTagsHandler(ucListTags))
	webSrv.AddListRoute('v', server.MethodPost, a.MakePostEncodeInlinesHandler(ucEvaluate))
	webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(ucEvaluate))
	webSrv.AddZettelRoute('x', server.MethodGet, api.MakeZettelContextHandler(ucZettelContext))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeListPlainHandler(ucListMeta))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetPlainZettelHandler(ucGetZettel))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('j', server.MethodPost, a.MakePostCreateZettelHandler(ucCreateZettel))
		webSrv.AddZettelRoute('j', server.MethodPut, api.MakeUpdateZettelHandler(ucUpdate))

Changes to cmd/cmd_run_simple.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
12
13
14
15
16
17
18

19
20
21
22
23
24
25

26
27
28

29
30
31
32
33
34
35
36







-







-
+


-
+








import (
	"flag"
	"fmt"
	"os"
	"strings"

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

func flgSimpleRun(fs *flag.FlagSet) {
	fs.String("d", "", "zettel directory")
}

func runSimpleFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) {
func runSimpleFunc(*flag.FlagSet) (int, error) {
	kern := kernel.Main
	listenAddr := kern.GetConfig(kernel.WebService, kernel.WebListenAddress).(string)
	exitCode, err := doRun(false)
	exitCode, err := doRun()
	if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
		kern.Log()
		kern.Log("--------------------------")
		kern.Log("Open your browser and enter the following URL:")
		kern.Log()
		kern.Log(fmt.Sprintf("    http://localhost%v", listenAddr[idx:]))
		kern.Log()

Changes to cmd/command.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
9
10
11
12
13
14
15


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 cmd

import (
	"flag"
	"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
	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

}

// CommandFunc is the function that executes the command.
// It accepts the parsed command line parameters.
// It returns the exit code and an error.
type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error)
type CommandFunc func(*flag.FlagSet) (int, error)

// GetFlags return the flag.FlagSet defined for the command.
func (c *Command) GetFlags() *flag.FlagSet { return c.flags }

var commands = make(map[string]Command)

// RegisterCommand registers the given command.

Changes to cmd/main.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
46
47
48
49
50
51
52
53
54

55
56
57
58
59
60
61
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51
52
53

54
55
56
57
58
59
60
61







-
+




















-
+









-
+







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

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

const (
	defConfigfile = ".zscfg"
)

func init() {
	RegisterCommand(Command{
		Name: "help",
		Func: func(*flag.FlagSet, *meta.Meta) (int, error) {
		Func: func(*flag.FlagSet) (int, error) {
			fmt.Println("Available commands:")
			for _, name := range List() {
				fmt.Printf("- %q\n", name)
			}
			return 0, nil
		},
	})
	RegisterCommand(Command{
		Name:   "version",
		Func:   func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil },
		Func:   func(*flag.FlagSet) (int, error) { return 0, nil },
		Header: true,
	})
	RegisterCommand(Command{
		Name:       "run",
		Func:       runFunc,
		Boxes:      true,
		Header:     true,
89
90
91
92
93
94
95
96

97
98
99
100
101
102
103
89
90
91
92
93
94
95

96
97
98
99
100
101
102
103







-
+







	} else {
		configFile = defConfigfile
	}
	content, err := os.ReadFile(configFile)
	if err != nil {
		return meta.New(id.Invalid)
	}
	return meta.NewFromInput(id.Invalid, input.NewInput(string(content)))
	return meta.NewFromInput(id.Invalid, input.NewInput(content))
}

func getConfig(fs *flag.FlagSet) *meta.Meta {
	cfg := readConfig(fs)
	fs.Visit(func(flg *flag.Flag) {
		switch flg.Name {
		case "p":
111
112
113
114
115
116
117

118


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








136
137
138

139
140
141
142
143
144
145
146
147
148
149

150
151
152
153


154
155
156
157
158
159
160
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

161
162
163
164

165
166
167
168
169
170
171
172
173







+

+
+

















+
+
+
+
+
+
+
+



+










-
+



-
+
+







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

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

func deleteConfiguredBoxes(cfg *meta.Meta) {
	for _, p := range cfg.PairsRest(false) {
		if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) {
			cfg.Delete(key)
		}
	}
}

const (
	keyAdminPort         = "admin-port"
	keyDebug             = "debug-mode"
	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"
	keyVerbose           = "verbose-mode"
)

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

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

241
242
243
244
245
246
247
248

249
250
251
252
253
254
255
254
255
256
257
258
259
260

261
262
263
264
265
266
267
268







-
+







	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)
	exitCode, err := command.Func(fs, cfg)
	exitCode, err := command.Func(fs)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
	}
	kernel.Main.Shutdown(true)
	return exitCode
}

Changes to config/config.go.

8
9
10
11
12
13
14

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







+







// under this license.
//-----------------------------------------------------------------------------

// Package config provides functions to retrieve runtime configuration data.
package config

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

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig
69
70
71
72
73
74
75
76

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

89
90
91
92
93
94
95
96
97

98
99
100
101
102
103
104
105
106

107
108
109
110
70
71
72
73
74
75
76

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

89
90
91
92
93
94
95
96
97

98
99
100
101
102
103
104
105
106

107
108
109
110
111







-
+











-
+








-
+








-
+




	// GetVisibility returns the visibility value of the metadata.
	GetVisibility(m *meta.Meta) meta.Visibility
}

// GetTitle returns the value of the "title" key of the given meta. If there
// is no such value, GetDefaultTitle is returned.
func GetTitle(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeyTitle); ok {
	if val, ok := m.Get(api.KeyTitle); ok {
		return val
	}
	if cfg != nil {
		return cfg.GetDefaultTitle()
	}
	return "Untitled"
}

// GetRole returns the value of the "role" key of the given meta. If there
// is no such value, GetDefaultRole is returned.
func GetRole(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeyRole); ok {
	if val, ok := m.Get(api.KeyRole); ok {
		return val
	}
	return cfg.GetDefaultRole()
}

// GetSyntax returns the value of the "syntax" key of the given meta. If there
// is no such value, GetDefaultSyntax is returned.
func GetSyntax(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeySyntax); ok {
	if val, ok := m.Get(api.KeySyntax); ok {
		return val
	}
	return cfg.GetDefaultSyntax()
}

// GetLang returns the value of the "lang" key of the given meta. If there is
// no such value, GetDefaultLang is returned.
func GetLang(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(meta.KeyLang); ok {
	if val, ok := m.Get(api.KeyLang); ok {
		return val
	}
	return cfg.GetDefaultLang()
}

Changes to docs/development/20210916193200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
1
2
3
4

5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
20




-
+







-
+







id: 20210916193200
title: Required Software
role: zettel
syntax: zmk
modified: 20210916194748
modified: 20211111143928

The following software must be installed:

* A current, supported [[release of Go|https://golang.org/doc/devel/release.html]],
* [[golint|https://github.com/golang/lint]],
* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go get honnef.co/go/tools/cmd/staticcheck``,
* [[unparam|mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``

Make sure that the software is in your path, e.g. via:

```sh
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin
```

Changes to docs/development/20210916194900.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
1
2
3
4

5
6



7
8
9

10
11
12
13
14
15
16
17




-
+

-
-
-



-
+







id: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk
modified: 20210917165448
modified: 20211110193448

# Check for unused parameters:
#* ``unparam ./...``
#* ``unparam -exported -tests ./...``
# Clean up your Go workspace:
#* ``go run tools/build.go clean`` (alternatively: ``make clean``).
# All internal tests must succeed:
#* ``go run tools/build.go check`` (alternatively: ``make check``).
#* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``).
# The API tests must succeed on every development platform:
#* ``go run tools/build.go testapi`` (alternatively: ``make api``).
# Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual:
#* ``go run -race cmd/zettelstore/main.go run -d docs/manual``
#* ``linkchecker  http://127.0.0.1:23123 2>&1 | tee lc.txt``
#* Check all ""Error: 404 Not Found""
#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''.
31
32
33
34
35
36
37
38



39
40
41
42
43
44
45
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44







-
+
+
+







#* download.wiki
#* changes.wiki
#* plan.wiki
# Set file ''VERSION'' to the new release version
# Disable Fossil autosync mode:
#* ``fossil setting autosync off``
# Commit the new release version:
#* ``fossil commit --tag release --tag version-VERSION -m "Version VERSION"``
#* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"``
#* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''.
   Otherwise client will not be able to import ''zettelkasten.de/z''.
# Clean up your Go workspace:
#* ``go run tools/build.go clean`` (alternatively: ``make clean``).
# Create the release:
#* ``go run tools/build.go release`` (alternatively: ``make release``).
# Remove previous executables:
#* ``fossil uv remove --glob '*-PREVVERSION*'``
# Add executables for release:

Changes to docs/manual/00001000000000.zettel.

1
2
3
4
5

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

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

19
20
21
22





+












-
+



id: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk
modified: 20211027121716

* [[Introduction|00001001000000]]
* [[Design goals|00001002000000]]
* [[Installation|00001003000000]]
* [[Configuration|00001004000000]]
* [[Structure of Zettelstore|00001005000000]]
* [[Layout of a zettel|00001006000000]]
* [[Zettelmarkup|00001007000000]]
* [[Other markup languages|00001008000000]]
* [[Security|00001010000000]]
* [[API|00001012000000]]
* [[Web user interface|00001014000000]]
* Troubleshooting
* [[Troubleshooting|00001018000000]]
* Frequently asked questions

Licensed under the EUPL-1.2-or-later.

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

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

27
28

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





-
+




















-
+

-
+




+
+
+
+
+
+
+







id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210712234656
modified: 20211109172143

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

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

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

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

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

  Default: ''0''
; [!box-uri-x]''box-uri-//X//'', where //X// is a number greater or equal to one
; [!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.
  During startup __X__ is counted up, starting with one, until no key is found.
  This allows to configure more than one box.

  If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ''dir://.zettel''.
  In this case, even a key ''box-uri-2'' will be ignored.
; [!debug-mode]''debug-mode''
: Allows to debug the Zettelstore software (mostly used by the developers).
  Disables any timeout values of the internal web server and does not send some security-related data.

  Do not enable it for a production server.

  Default: ''false''
; [!default-dir-box-type]''default-dir-box-type''
: Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]].
  Zettel are typically stored in such boxes.

  Default: ''notify''
; [!insecure-cookie]''insecure-cookie''
: Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP).
76
77
78
79
80
81
82
83
84


85
83
84
85
86
87
88
89


90
91
92







-
-
+
+

  It is automatically extended, when a new HTML view is rendered.
; [!url-prefix]''url-prefix''
: Add the given string as a prefix to the local part of a Zettelstore local URL/URI when rendering zettel representations.
  Must begin and end with a slash character (""''/''"", ''U+002F'').
  Default: ''"/"''.

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

Changes to docs/manual/00001004011200.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22

23
24
25
26
27
28
29
30

31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21

22
23
24
25
26
27
28
29

30
31
32
33
34

35
36
37
38
39
40
41
42
43
44
45
46





-
+









-
+





-
+







-
+




-
+











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

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

An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore.
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//{-}.
To cope with these (and more) situations, you configure Zettelstore to use one or more __boxes__.
This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number).
Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses.

The following box URIs are supported:

; ''dir:\//DIR''
; [!dir]''dir:\//DIR''
: Specifies a directory where zettel files are stored.
  ''DIR'' is the file path.
  Although it is possible to use relative file paths, such as ''./zettel'' (&rarr; URI is ''dir:\//.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''.

  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.
; ''file:FILE.zip'' oder ''file:/\//path/to/file.zip''
; [!file]''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.
; ''mem:''
; [!mem]''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.

If you use the ''mem:'' box, where zettel are stored in volatile memory, it makes only sense if you configure it as ''box-uri-1''.
Such a box will be empty when Zettelstore starts and only the first box will receive updates.
You must make sure that your computer has enough RAM to store all zettel.

Changes to docs/manual/00001004051000.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: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210712234419
modified: 20211109172046

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

```
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]
```
39
40
41
42
43
44
45
46

47
48
39
40
41
42
43
44
45

46
47
48







-
+


  If you want to listen on network card to process requests from other computer, please use [[''listen-addr''|00001004010000#listen-addr]] of the configuration file as described below.
; [!r]''-r''
: Puts the Zettelstore in read-only mode.
  No changes are possible via the web interface / via the API.

  This allows to publish your content without any risks of unauthorized changes.
; [!v]''-v''
: Be more verbose in writing logs.
: Be more verbose when writing logs.

Command line options take precedence over [[configuration file|00001004010000]] options.

Changes to docs/manual/00001004100000.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
1
2
3
4
5

6
7
8
9
10
11
12
13

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





-
+







-
+











id: 00001004100000
title: Zettelstore Administrator Console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210510155859
modified: 20211103162926

The administrator console is a service accessible only on the same computer on which Zettelstore is running.
It allows an experienced user to monitor and control some of the inner workings of Zettelstore.

You enable the administrator console by specifying a TCP port number greater than zero (better: greater than 1024) for it, either via the [[command-line parameter ''-a''|00001004051000#a]] or via the ''admin-port'' key of the [[startup configuration file|00001004010000#admin-port]].

After you enable the administrator console, you can use tools such as [[PuTTY|https://www.chiark.greenend.org.uk/~sgtatham/putty/]] or other telnet software to connect to the administrator console.
In fact, the administrator console is //not// a full telnet service.
In fact, the administrator console is __not__ a full telnet service.
It is merely a simple line-oriented service where each input line is interpreted separately.
Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc.

After connecting to the administrator console, there is no further authentication.
It is not needed because you must be logged in on the same computer where Zettelstore is running.
You cannot connect to the administrator console if you are on a different computer.
Of course, on multi-user systems with untrusted users, you should not enable the administrator console.

* Enable via [[command line|00001004051000#a]]
* Enable via [[configuration file|00001004010000#admin-port]]
* [[List of supported commands|00001004101000]]

Changes to docs/manual/00001004101000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001004101000
title: List of supported commands of the administrator console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210525161623
modified: 20211103161956

; ''bye''
: Closes the connection to the administrator console.
; ''config SERVICE''
: Displays all valid configuration keys for the given service.

  If a key ends with the hyphen-minus character (""''-''"", ''U+002D''), the key denotes a list value.
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 box box-uri-0 any_text`` will remove all values of the list __box-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
1
2
3
4
5

6
7
8
9
10
11
12
13





-
+







id: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20210614165848
modified: 20211103163918

Zettelstore is a software that manages your zettel.
Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories.
Typically, file names and file content must comply to specific rules so that Zettelstore can manage them.
If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions.

Zettelstore provides additional services to the user.
28
29
30
31
32
33
34
35

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

35
36
37
38
39
40
41
42







-
+








Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]].
If you create a new zettel via the web 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.

Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.
The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel.
You can create these special zettel identifiers either with the //rename// function of Zettelstore or by manually renaming the underlying zettel files.
You can create these special zettel identifiers either with the __rename__ function of Zettelstore or by manually renaming the underlying zettel files.

It is allowed that the file name contains other characters after the 14 digits.
These are ignored by Zettelstore.

The file name must have an file extension.
Two file extensions are used by Zettelstore: ''.meta'' and ''.zettel''.
Other file extensions are used to determine the ""syntax"" of a zettel.
74
75
76
77
78
79
80
81

82
83
84
85

86
87
88
89
74
75
76
77
78
79
80

81
82
83
84

85
86
87
88
89







-
+



-
+




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.]
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//).
For testing purposes, zettel may be stored in volatile memory (called __RAM__).
This way is called ""memory box"".

Other types of boxes could be added to Zettelstore.
What about a ""remote Zettelstore box""?

Changes to docs/manual/00001006010000.zettel.

1
2

3
4
5

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

20
21
22
23
24
25
26
1
2
3
4
5

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

20
21
22
23
24
25
26
27


+


-
+













-
+







id: 00001006010000
title: Syntax of Metadata
role: manual
tags: #manual #syntax #zettelstore
syntax: zmk
role: manual
modified: 20211103164152

The metadata of a zettel is a collection of key-value pairs.
The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]).

The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"") is also allowed.
It begins at the first position of a new line.

A key is separated from its value either by
* a colon character (""'':''""),
* a non-empty sequence of space characters,
* a sequence of space characters, followed by a colon, followed by a sequence of space characters.

A Value is a sequence of printable characters.
If the value should be continued in the following line, that following line (//continuation line//) must begin with a non-empty sequence of space characters.
If the value should be continued in the following line, that following line (""continuation line"") must begin with a non-empty sequence of space characters.
The rest of the following line will be interpreted as the next part of the value.
There can be more than one continuation line for a value.

A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line.
It will be ignored.

Parsing metadata ends, if an empty line is found or if a line with at least three hyphen-minus characters is found.

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6

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




21
22
23
24
25
26
27
28
29
30




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

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

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: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20210822234119
modified: 20211103163613

Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore.
See the [[computed list of supported metadata keys|00000000000090]] for details.

Most keys conform to a [[type|00001006030000]].

; [!all-tags]''all-tags''
: A property (a computed values that is not stored) that contains both the value of [[''tags''|#tags]] together with all [[tags|00001007040000#tag]] that are specified within the content.
; [!back]''back''
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel.
  Basically, it is the value of [[''backward''|#bachward]], but without any zettel identifier that is contained in [[''forward''|#forward]].
; [!backward]''backward''
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata.
  References within inversable values are not included here, e.g. [[''precursor''|#precursor]].
  References within invertible values are not included here, e.g. [[''precursor''|#precursor]].
; [!box-number]''box-number''
: Is a computed value and contains the number of the box where the zettel was found.
  For all but the [[predefined zettel|00001005090000]], this number is equal to the number __X__ specified in startup configuration key [[''box-uri-__X__''|00001004010000#box-uri-x]].
; [!copyright]''copyright''
: Defines a copyright string that will be encoded.
  If not given, the value ''default-copyright'' from the  [[configuration zettel|00001004020000#default-copyright]] will be used.
; [!credential]''credential''
: Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]].
  It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key.

  It is only used for zettel with a ''role'' value of ""user"".
; [!dead]''dead''
: Property that contains all references that does //not// identify a zettel.
: Property that contains all references that does __not__ identify a zettel.
; [!duplicates]''duplicates''
: Is set to the value ""true"" if there is more than one file that could contain the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].
; [!folge]''folge''
: Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value.
; [!forward]''forward''
: Property that contains all references that identify another zettel within the content of the zettel.
; [!id]''id''
: Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore.
  It cannot be set manually, because it is a computed value.
46
47
48
49
50
51
52
53

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

59



60
61
62
63
64
65
66







-
+
-
-
-







; [!modified]''modified''
: Date and time when a zettel was modified through Zettelstore.
  If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value.

  This is a computed value.
  There is no need to set it via Zettelstore.
; [!no-index]''no-index''
: If set to true, the zettel will not be indexed and therefore not be found in full-text searches.
: If set to a ""true"" value, 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.
  Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used.

Changes to docs/manual/00001006055000.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: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20210917154229
modified: 20211001125243

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

36
37
38
39
40
41
42
43

36
37
38
39
40
41
42

43







-
+
| 00000200000000 | 0000899999999 | Reserved for future use
| 00009000000000 | 0000999999999 | Reserved for applications

This list may change in the future.

==== External Applications
|= From | To | Description
| 00009000001000 | 00009000001999 | ZS Slides, an application to display zettel as a HTML-based slideshow
| 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow

Changes to docs/manual/00001007000000.zettel.

1
2

3
4
5

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

25
26
27
28
29
30
31
32
33
34
35
36
1
2
3
4
5

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

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


+


-
+


















-
+












id: 00001007000000
title: Zettelmarkup
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
modified: 20211103162744

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

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

Zettelmarkup is a markup language on its own.
This is in contrast to Markdown, which is basically a superset of HTML.
While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX.
Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript.
This could create problems with longevity as well as security problems.

Zettelmarkup is a rich markup language, but it focusses on relatively short zettel content.
It allows embedding other content, simple tables, quotations, description lists, and images.
It provides a broad range of inline formatting, including //emphasized//{-}, **strong**{-}, __underlined__, ;;small;;, ~~deleted~~{-} and __inserted__{-} text.
It provides a broad range of inline formatting, including __emphasized__, **strong**, ;;small;;, ~~deleted~~{-} and >>inserted>> text.
Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys.

Zettelmarkup might be seen as a proprietary markup language.
But if you want to use Markdown/CommonMark and you need support for footnotes, you'll end up with a proprietary extension.
However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds.

* [[General principles|00001007010000]]
* [[Basic definitions|00001007020000]]
* [[Block-structured elements|00001007030000]]
* [[Inline-structured element|00001007040000]]
* [[Attributes|00001007050000]]
* [[Summary of formatting characters|00001007060000]]

Changes to docs/manual/00001007030100.zettel.

1
2

3
4
5

6
7
8
9
10

11
12
13
14
15
16
17
1
2
3
4
5

6
7
8
9
10

11
12
13
14
15
16
17
18


+


-
+




-
+







id: 00001007030100
title: Zettelmarkup: Description Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
modified: 20211103163447

A description list is a sequence of terms to be described together with the descriptions of each term.
Every term can described in multiple ways.

A description term (short: //term//) is specified with one semicolon (""'';''"", ''U+003B'') at the first position, followed by a space character and the described term, specified as a sequence of line elements.
A description term (short: __term__) is specified with one semicolon (""'';''"", ''U+003B'') at the first position, followed by a space character and the described term, specified as a sequence of line elements.
If the following lines should also be part of the term, exactly two spaces must be given at the beginning of each following line.

The description of a term is given with one colon (""'':''"", ''U+003A'') at the first position, followed by a space character and the description itself, specified as a sequence of inline elements.
Similar to terms, following lines can also be part of the actual description, if they begin at each line with exactly two space characters.

In contrast to terms, the actual descriptions are merged into a paragraph.
This is because, an actual description can contain more than one paragraph.

Changes to docs/manual/00001007030200.zettel.

1
2

3
4
5

6
7
8
9
10

11
12
13
14
15
16
17
1
2
3
4
5

6
7
8
9
10

11
12
13
14
15
16
17
18


+


-
+




-
+







id: 00001007030200
title: Zettelmarkup: Nested Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
modified: 20211103162835

There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists.

Ordered lists are specified with the number sign (""''#''"", ''U+0023''), unordered lists use the asterisk (""''*''"", ''U+002A''), and quotation lists are specified with the greater-than sing (""''>''"", ''U+003E'').
Let's call these three characters //list characters//.
Let's call these three characters __list characters__.

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

Changes to docs/manual/00001007030300.zettel.

1
2

3
4
5

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

6
7
8
9
10
11
12
13


+


-
+







id: 00001007030300
title: Zettelmarkup: Headings
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
modified: 20211103163105

To specify a (sub-) section of a zettel, you should use the headings syntax: at
the beginning of a new line type at least three equal signs (""''=''"", ''U+003D''), plus at least one
space and enter the text of the heading as inline elements.

```zmk
=== Level 1 Heading
28
29
30
31
32
33
34
35

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

36
37
38
39
40
41
42







-
+







=== Notes

The heading level is translated to a HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==<h2>Level 1 Heading</h2>=={=html}.
The ==<h1>=={=html} tag is rendered for the zettel title.

This syntax is often used in a similar way in wiki implementation.
However, trailing equal signs are //not//{-} removed, they are part of the heading text.
However, trailing equal signs are __not__ removed, they are part of the heading text.

If you use command line tools, you can easily create a draft table of contents with the command:

```sh
grep -h '^====* ' ZETTEL_ID.zettel
```

Changes to docs/manual/00001007031000.zettel.

1
2
3
4
5
6

7
8
9

10
11

12
13
14
15
16
17
18
1
2
3
4
5

6
7
8

9
10

11
12
13
14
15
16
17
18





-
+


-
+

-
+







id: 00001007031000
title: Zettelmarkup: Tables
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20210523185812
modified: 20211103161859

Tables are used to show some data in a two-dimenensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering //table rows//.
In zettelmarkup, table are not specified explicitly, but by entering __table rows__.
Therefore, a table can be seen as a sequence of table rows.
A table row is nothing as a sequence of //table cells//.
A table row is nothing as a sequence of __table cells__.
The length of a table is the number of table rows, the width of a table is the maximum length of its rows.

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







-
+







:::example
| a1 | a2 | a3|
| b1 | b2 | b3
| c1 | c2
:::

=== Header row
If any cell in the first row of a table contains an equal sing character (""''=''"", ''U+003D'') as the very first character, then this first row will be interpreted as a //table header// row.
If any cell in the first row of a table contains an equal sing character (""''=''"", ''U+003D'') as the very first character, then this first row will be interpreted as a __table header__ row.

For example:
```zmk
| a1 | a2 |= a3|
| b1 | b2 | b3
| c1 | c2
```

Changes to docs/manual/00001007040000.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: 00001007040000
title: Zettelmarkup: Inline-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20210822234205
modified: 20211103162150

Most characters you type is concerned with inline-structured elements.
The content of a zettel contains is many cases just ordinary text, lightly formatted.
Inline-structured elements allow to format your text and add some helpful links or images.
Sometimes, you want to enter characters that have no representation on your keyboard.

; Text formatting
31
32
33
34
35
36
37
38

39
40
41
42
43

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

63
64
65
66
67
68
31
32
33
34
35
36
37

38
39
40
41
42

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

62
63
64
65
66
67
68







-
+




-
+


















-
+






==== Comments
A comment begins with two consecutive percent sign characters (""''%''"", ''U+0025'').
It ends at the end of the line where it begins.

==== Backslash
The backslash character (""''\\''"", ''U+005C'') gives the next character another meaning.
* If a space character follows, it is converted in a non-breaking space (''U+00A0'').
* If a line ending follows the backslash character, the line break is converted from a //soft break// into a //hard break//.
* If a line ending follows the backslash character, the line break is converted from a __soft break__ into a __hard break__.
* Every other character is taken as itself, but without the interpretation of a Zettelmarkup element.
  For example, if you want to enter a ""'']''"" into a footnote text, you should escape it with a backslash.

==== Tag
Any text that begins with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an //inline tag//.
Any text that begins with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an __inline tag__.
They are be considered equivalent to tags in metadata.

==== Entities & more
Sometimes it is not easy to enter special characters.
If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name.

Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B'').
If you know the HTML name of the character you want to enter, put it between these two character.
Example: ``&amp;`` is rendered as ::&amp;::{=example}.

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

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

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

* Two consecutive hyphen-minus characters result in an //en-dash// character.
* Two consecutive hyphen-minus characters result in an __en-dash__ character.
  It is typically used in numeric ranges.
  ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}.
  Alternative specifications are: ``&ndash;``, ``&x8211``, and ``&#x2013``.
* Three consecutive full stop characters (""''.''"", ''U+002E'') after a space result in an horizontal ellipsis character.
  ``to be continued ... later`` will be rendered in HTML as: ::to be continued, ... later::{=example}.
  Alternative specifications are: ``&hellip;``, ``&x8230``, and ``&#x2026``.

Changes to docs/manual/00001007040100.zettel.

1
2

3
4
5

6
7
8
9
10
11
12
13
14
15


16
17



18
19

20
21
22
23


24
25

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

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


19
20
21


22
23



24
25


26
27

28
29
30
31
32
33
34


+


-
+










+
+
-
-
+
+
+
-
-
+

-
-
-
+
+
-
-
+

-







id: 00001007040100
title: Zettelmarkup: Text Formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual
modified: 20211113175248

Text formatting is the way to make your text visually different.
Every text formatting element begins with two same characters.
It ends when these two same characters occur the second time.
It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character.

Text formatting can be nested, up to a reasonable limit.

The following characters begin a text formatting:

* The low line character (""''_''"", ''U+005F'') emphasizes its text.
** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}.
* The slash character (""''/''"", ''U+002F'') emphasizes its text. Often, such text is rendered in italics. If the default attribute is specified, the emphasized text is not just rendered as such, but also internally marked as emphasized.
** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}.
** The slash character (""''/''"", ''U+002F'') is still allowed to emphasize text, but its use is deprecated.
   With [[version 0.2.0|https://zettelstore.de/home/doc/trunk/www/changes.wiki#0_2_0]] it will no longer be supported. 
*** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}.
** Example: ``abc //def//{-} ghi`` is rendered in HTML as: ::abc //def//{-} ghi::{=example}.
* The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text. The text is often rendered in bold. Again, the default attribute will force a explicit semantic meaning of strong emphasizing.
* The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text.
** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}.
** Example: ``abc **def**{-} ghi`` is rendered in HTML as: ::abc **def**{-} ghi::{=example}.
* The low line character (""''_''"", ''U+005F'') produces underlined text. If the default attribute was specified, it is semantically rendered as inserted text.
** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}.
* The greater-than sign character (""''>''"", ''U+003E'') marks text as inserted.
** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}.
** Example: ``abc __def__{-} ghi`` is rendered in HTML as: ::abc __def__{-} ghi::{=example}.
* Similar, the tilde character (""''~''"", ''U+007E'') produces text that is strike-through. If the default attribute was specified, it is semantically rendered as deleted text.
* Similar, the tilde character (""''~''"", ''U+007E'') marks deleted text.
** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}.
** Example: ``abc ~~def~~{-} ghi`` is rendered in HTML as: ::abc ~~def~~{-} ghi::{=example}.
* The apostrophe character (""''\'''"", ''U+0027'') renders text in mono-space / fixed font width.
** Example: ``abc ''def'' ghi`` is rendered in HTML as: ::abc ''def'' ghi::{=example}.
* The circumflex accent character (""''^''"", ''U+005E'') allows to enter superscripted text.
** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}.
* The comma character (""'',''"", ''U+002C'') produces subscripted text.
** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}.
* The less-than sign character (""''<''"", ''U+003C'') marks an inline quotation.

Changes to docs/manual/00001007040322.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26
1
2
3
4
5

6
7
8
9
10
11
12
13

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





-
+







-
+












id: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20210811165719
modified: 20211103163657

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

Supported formats are:

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

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

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

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

Changes to docs/manual/00001007040350.zettel.

1
2
3
4
5

6
7
8

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

9
10
11
12
13
14
15
16
17
18





+


-
+









id: 00001007040350
title: Zettelmarkup: Mark
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20211103163338

A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a bigger-sized zettel, currently with a [[link|00001007040310]] only[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called //transclusion//.].
This is useful if you want to reference some content in a bigger-sized zettel, currently with a [[link|00001007040310]] only[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called __transclusion__.].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", ''U+0021'').
Now the optional mark name follows.
It is a (possibly empty) sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low-line character (""''_''"", ''U+005F'').
The mark element ends with a right square bracket.

Examples:
* ``[!]`` is a mark without a name, the empty mark.
* ``[!mark]`` is a mark with the name ""mark"".

Changes to docs/manual/00001007050000.zettel.

1
2
3
4
5
6

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

28
29
30
31
32
33
34
35







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

51
52
53
54
55
56
57
1
2
3
4
5

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

27
28
29






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

51
52
53
54
55
56
57
58





-
+




















-
+


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














-
+







id: 00001007050000
title: Zettelmarkup: Attributes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20210830161438
modified: 20211103162617

Attributes allows to modify the way how material is presented.
Alternatively, they provide additional information to markup elements.
To some degree, attributes are similar to [[HTML attributes|https://html.spec.whatwg.org/multipage/dom.html#global-attributes]].

Typical use cases for attributes are to specify the (natural) [[language|00001007050100]] for a text region, to specify the [[programming language|00001007050200]] for highlighting program code, or to make white space visible in plain text.

Attributes are specified within curly brackets ``{...}``.
Of course, more than one attribute can be specified.
Attributes are separated by a sequence of space characters or by a comma character.

An attribute normally consists of an optional key and an optional value.
The key is a sequence of letters, digits, a hyphen-minus (""''-''"", ''U+002D'', and a low line / underscore (""''_''"", ''U+005D'').
It can be empty.
The value is a sequence of any character, except space and the right curly bracket (""''}''"", ''U+007D'').
If the value must contain a space or the right curly bracket, the value can be specified within two quotation marks (""''"''"", ''U+0022'').
Within the quotation marks, the backslash character functions as an escape character to specify the quotation mark (and the backslash character too).

Some examples:

* ``{key=value}`` sets the attribute //key// to value //value//.
* ``{key=value}`` sets the attribute __key__ to value __value__.
* ``{key="value with space"}`` sets the attribute to the given value.
* ``{key="value with quote \\" (and backslash \\\\)"}``
* ``{name}`` sets the attribute //name//. It has no corresponding value. It is
  equivalent to ``{name=}``.
* ``{=key}`` sets the //generic attribute//{-} to the given value. It is mostly
  used for modifying behaviour according to a programming language.
* ``{.key}`` sets the //class attribute//{-} to the given value. It is
  equivalent to ``{class=key}``. 
* ``{name}`` sets the attribute __name__.
  It has no corresponding value.
  It is equivalent to ``{name=}``.
* ``{=key}`` sets the __generic attribute__ to the given value.
  It is mostly used for modifying behaviour according to a programming language.
* ``{.key}`` sets the __class attribute__ to the given value.
  It is equivalent to ``{class=key}``. 

In these examples, ``key`` must conform the the syntax of attribute keys, even if it is used as a value.

If a key is given more than once in an attribute, the values are concatenated (and separated by a space).

* ``{key=value1 key=value2}`` is the same as ``{key"value1 value2"}``.
* ``{key key}`` is the same as ``{key}``.
* ``{.class1 .class2}`` is equivalent to ``{class="class1 class2"}``.

This is not true for the generic attribute.
In ``{=key1 =key2}``, the first key is ignored.
Therefore it is equivalent to ``{=key2}``.

The key ""''-''"" (just hyphen-minus) is special.
It is called //default attribute//{-} and has a markup specific meaning.
It is called __default attribute__ and has a markup specific meaning.
For example, when used for plain text, it replaces the non-visible space with a visible representation:

* ++``Hello, world``{-}++ produces ==Hello, world=={-}.
* ++``Hello, world``++ produces ==Hello, world==.

For some block elements, there is a syntax variant if you only want to specify a generic attribute.
For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) block characters.

Changes to docs/manual/00001007060000.zettel.

1
2
3
4
5
6

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

21
22
23
24
25

26
27
28
29
30

31
32
33
34
35
36
37

38
39
40
41
42

1
2
3
4
5

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

20
21
22
23
24

25
26
27
28
29

30
31
32
33
34
35
36

37
38
39
40
41

42





-
+













-
+




-
+




-
+






-
+




-
+
id: 00001007060000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
modified: 20210728162830
modified: 20211113180517

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

|= Character :|= Blocks <|= Inlines <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Typographic quotation mark|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[Tag|00001007040000]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]
| ''&''  | (free) | [[Entity|00001007040000]]
| ''\'''  | (free)  | [[Monospace text|00001007040100]]
| ''(''  | (free) | (free)
| '')''  | (free) | (free)
| ''*''  | [[Unordered list|00001007030200]] | [[strongly emphasized / bold text|00001007040100]]
| ''*''  | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]]
| ''+''  | (free) | [[Keyboard input|00001007040200]]
| '',''  | (free) | [[Subscripted text|00001007040100]]
| ''-''  | [[Horizonal rule|00001007030400]] | ""[[en-dash|00001007040000]]""
| ''.''  | (free) | [[Horizontal ellipsis|00001007040000]]
| ''/''  | (free) | [[Emphasized / italics text|00001007040100]]
| ''/''  | (free) | [[//Emphasized text//|00001007040100]] (deprecated for v0.2.0)
| '':''  | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]]
| '';''  | [[Description term|00001007030100]] | [[Small text|00001007040100]]
| ''<''  | [[Quotation block|00001007030600]] | [[Short inline quote|00001007040100]]
| ''=''  | [[Headings|00001007030300]] | [[Computer output|00001007040200]]
| ''>''  | [[Quotation lists|00001007030200]] | (blocked: to remove anyambiguity with quotation lists)
| ''>''  | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]]
| ''?''  | (free) | (free)
| ''@''  | (free) | (reserved)
| ''[''  | (reserved)  | [[Linked material|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]]
| '']''  | (reserved) | End of link, citation key, footnote, mark
| ''^''  | (free) | [[Superscripted text|00001007040100]]
| ''_''  | (free) | [[Underlined text|00001007040100]]
| ''_''  | (free) | [[Emphasized text|00001007040100]]
| ''`''  | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]]
| ''{''  | (reserved) | [[Embedded material|00001007040300]], [[Attribute|00001007050000]]
| ''|''  | [[Table row / table cell|00001007031000]] | Separator within link and embed formatting
| ''}''  | (reserved) | End of embedded material, End of Attribute
| ''~''  | (free) | [[Strike-through text|00001007040100]]
| ''~''  | (free) | [[Deleted text|00001007040100]]

Changes to docs/manual/00001010070400.zettel.

1
2

3
4
5

6
7

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

6
7

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


+


-
+

-
+












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

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

; Not read-only, no authorization
: Zettelstore runs on your local computer and you only work with it.
; Not read-only, with authorization
: Zettelstore is accessed remotely.
  You need authentication to ensure that only valid users access your Zettelstore.
; With read-only, no authorization
: Zettelstore present publicly its full content to everybody.
; With read-only, with authorization
: Nobody is allowed to change the content of the Zettelstore, but only specific zettel should be presented to the public.

Changes to docs/manual/00001010090100.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: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
modified: 20210511131719
modified: 20211027125733

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

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key.
Technically, this is not trivial.
63
64
65
66
67
68
69
70

63
64
65
66
67
68
69

70







-
+
  }
}
```
This will forwards requests with the prefix ""/manual/"" to the running Zettelstore.
All other requests will be handled by Caddy itself.

In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"".
This is to allow Zettelstore ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response.
This is to allow Zettelstore to ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response.

Changes to docs/manual/00001012000000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







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

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

=== Background
The API is HTTP-based and uses plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software.
28
29
30
31
32
33
34
35


36
37
38
39
40
41
42
43



28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44
45
46
47







-
+
+








+
+
+
** [[Content search|00001012051840]]
** [[Sort the list of zettel metadata|00001012052000]]
* [[List all tags|00001012052400]]
* [[List all roles|00001012052600]]

=== Working with zettel
* [[Create a new zettel|00001012053200]]
* [[Retrieve metadata and content of an existing zettel|00001012053400]]
* [[Retrieve metadata and content of an existing zettel|00001012053300]]
* [[Retrieve metadata of an existing zettel|00001012053400]]
* [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]]
* [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]]
* [[Retrieve references of an existing zettel|00001012053700]]
* [[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]]

=== Various helper method
* [[Encode Zettelmarkup inline material as HTML/Text|00001012070500]]

Changes to docs/manual/00001012051200.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13

14
15
16
17

18




19
20
21
22
23

24
25
26
27
28
29
30
1
2
3
4
5

6
7
8
9
10
11
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: 00001012051200
title: API: List metadata of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210905203616
modified: 20211004124401

To list the metadata of all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/j''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/j
{"list":[{"id":"00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]}
{"query":"","list":[{"id":"00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]}
```

The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects.
These zettel JSON objects themself contains the keys ''"id"'' (value is a string containing the zettel identifier), , and ''"meta"'' (value as a JSON object).
These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the zettel identifier), , and ''"meta"'' (value as a JSON object).
The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings.

Additionally, the JSON object contains a key ''"query"'' with a string value.
It will contain a textual description of the underlying query if you [[select only some zettel|00001012051810]].
Without a selection, the value is the empty string.

If you reformat the JSON output from the ''GET /j'' call, you'll see its structure better:

```json
{
  "query": "",
  "list": [
    {
      "id": "00001012051200",
      "meta": {
        "title": "API: List for all zettel some data",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",

Changes to docs/manual/00001012051800.zettel.

1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
1
2
3
4
5

6
7

8
9
10
11
12
13
14
15





-
+

-
+







id: 00001012051800
title: API: Shape the list of zettel metadata
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210905203719
modified: 20211103162259

In most cases, it is not essential to list //all// zettel.
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 /j'' request.

* [[Select|00001012051810]] just some zettel, based on metadata.
* 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.

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

6
7

8
9
10
11
12
13
14
15

16
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: 00001012051810
title: API: Select zettel based on their metadata
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210905203929
modified: 20211103164030

Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key.
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/j?title=API'
{"list":[{"id":"00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ...
{"query":"title MATCH API","list":[{"id":"00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ...
```

However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021'').
However, if you want all zettel that does not match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021'').
For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/j?title=!API'
{"list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}},
{"query":"title NOT MATCH API","list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}},
...
```

In both cases, an implicit precondition is that the zettel must contain the given metadata key.
For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true.
But the situation is different for a key like [[''url''|00001006020000#url]].
Both ``curl 'http://localhost:23123/j?url='`` and ``curl 'http://localhost:23123/j?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.
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/j?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/j?url=!com'`` and ``curl 'http://127.0.0.1:23123/j?url=com&_negate'`` may produce different lists.

Changes to docs/manual/00001012051830.zettel.

1
2
3
4
5
6

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





-
+





-
+












-
+







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

=== Limit
By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements:
```sh
# curl 'http://127.0.0.1:23123/j?title=API&_sort=id&_limit=2'
{"list":[{"id":"00001012000000","meta":{"all-tags":"#api #manual #zettelstore","back":"00001000000000 00001004020000","backward":"00001000000000 00001004020000 00001012053200 00001012054000 00001014000000","box-number":"1","forward":"00001010040100 00001010040700 00001012050200 00001012050400 00001012050600 00001012051200 00001012051800 00001012051810 00001012051830 00001012051840 00001012052000 00001012052200 00001012052400 00001012052600 00001012053200 00001012053400 00001012053500 00001012053600 00001012053700 00001012053800 00001012054000 00001012054200 00001012054400 00001012054600 00001012920000 00001014000000","modified":"20210817160844","published":"20210817160844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API"}},{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}}]}
{"query":"title MATCH API LIMIT 2","list":[{"id":"00001012000000","meta":{"all-tags":"#api #manual #zettelstore","back":"00001000000000 00001004020000","backward":"00001000000000 00001004020000 00001012053200 00001012054000 00001014000000","box-number":"1","forward":"00001010040100 00001010040700 00001012050200 00001012050400 00001012050600 00001012051200 00001012051800 00001012051810 00001012051830 00001012051840 00001012052000 00001012052200 00001012052400 00001012052600 00001012053200 00001012053300 00001012053500 00001012053600 00001012053700 00001012053800 00001012054000 00001012054200 00001012054400 00001012054600 00001012920000 00001014000000","modified":"20210817160844","published":"20210817160844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API"}},{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}}]}
```

```sh
# curl 'http://127.0.0.1:23123/z?title=API&_sort=id&_limit=2'
00001012000000 API
00001012050200 API: Authenticate a client
```

=== Offset
The query parameter ""''_offset''"" allows to list not only the first elements, but to begin at a specific element:
```sh
# curl 'http://127.0.0.1:23123/j?title=API&_sort=id&_limit=2&_offset=1'
{"list":[{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}},{"id":"00001012050400","meta":{"all-tags":"#api #manual #zettelstore","back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20210726123745","published":"20210726123745","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"}}]}
{"query":"title MATCH API OFFSET 1 LIMIT 2","list":[{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}},{"id":"00001012050400","meta":{"all-tags":"#api #manual #zettelstore","back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20210726123745","published":"20210726123745","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"}}]}
```

```sh
# curl 'http://127.0.0.1:23123/z?title=API&_sort=id&_limit=2&_offset=1'
00001012050200 API: Authenticate a client
00001012050400 API: Renew an access token
```

Added docs/manual/00001012053300.zettel.















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012053300
title: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20211004111804

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

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/j/00001012053300
{"id":"00001012053300","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ...
```

Pretty-printed, this results in:
```
{
  "id": "00001012053300",
  "meta": {
    "back": "00001012000000 00001012053200 00001012054400",
    "backward": "00001012000000 00001012053200 00001012054400 00001012920000",
    "box-number": "1",
    "forward": "00001010040100 00001012050200 00001012920000 00001012920800",
    "modified": "20210726190012",
    "published": "20210726190012",
    "role": "manual",
    "syntax": "zmk",
    "tags": "#api #manual #zettelstore",
    "title": "API: Retrieve metadata and content of an existing zettel"
  },
  "encoding": "",
  "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' (...)
}
```

[!plain]Additionally, you can retrieve the plain zettel, without using JSON.
Just change the [[endpoint|00001012920000]] to ''/z/{ID}''
Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''_part=[[PART|00001012920800]]''.
Valid values are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value).

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

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

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

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

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

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

Changes to docs/manual/00001012053400.zettel.

1
2

3
4
5
6

7
8

9
10
11
12
13
14


15
16
17
18
19
20
21

22
23


24
25
26
27


28
29
30
31

32
33
34

35
36
37
38
39


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

57
58
59
60
61
62
63
1

2
3
4
5

6
7

8
9
10
11
12


13
14
15
16
17
18
19

20
21


22
23
24
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: 00001012053400
title: API: Retrieve metadata and content of an existing zettel
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210905204428
modified: 20211004112257

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).
The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/m/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/j/00001012053400
{"id":"00001012053400","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ...
# curl http://127.0.0.1:23123/m/00001012053400
{"meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012920800","modified":"20211004111240","published":"20211004111240","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"}}
```

Pretty-printed, this results in:
```
{
  "id": "00001012053400",
  "meta": {
    "all-tags": "#api #manual #zettelstore",
    "back": "00001012000000 00001012053200 00001012054400",
    "backward": "00001012000000 00001012053200 00001012054400 00001012920000",
    "back": "00001012000000 00001012053300",
    "backward": "00001012000000 00001012053300 00001012920000",
    "box-number": "1",
    "forward": "00001010040100 00001012050200 00001012920000 00001012920800",
    "modified": "20210726190012",
    "published": "20210726190012",
    "modified": "20211004111240",
    "published": "20211004111240",
    "role": "manual",
    "syntax": "zmk",
    "tags": "#api #manual #zettelstore",
    "title": "API: Retrieve metadata and content of an existing zettel"
    "title": "API: Retrieve metadata of an existing zettel"
  },
  "encoding": "",
  "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' (...)
  }
}
```

[!plain]Additionally, you can retrieve the plain zettel, without using JSON.
Just change the [[endpoint|00001012920000]] to ''/z/{ID}''
[!plain]Additionally, you can retrieve the plain metadata of a zettel, without using JSON.
Just change the [[endpoint|00001012920000]] to ''/z/{ID}?_part=meta''
Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''_part=[[PART|00001012920800]]''.
Valid values are ""zettel"", ""meta"", and ""content"" (the default value).

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

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
...
````

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

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

Changes to docs/manual/00001012053500.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: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210826224328
modified: 20211028191044

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

For example, to retrieve some evaluated data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/v/00001012053500
31
32
33
34
35
36
37
38

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

38
39
40
41
42
43
44
45







-
+







<meta name="zs-backward" content="00001012000000">
<meta name="zs-box-number" content="1">
<meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800">
<meta name="zs-published" content="00001012053500">
</head>
<body>
<p>The <a href="/v/00001012920000?_enc=html">endpoint</a> to work with metadata and content of a specific zettel is <span style="font-family:monospace">/v/{ID}</span>, where <span style="font-family:monospace">{ID}</span> is a placeholder for the zettel identifier (14 digits).</p>
<p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <span class="zs-monospace">/v/{ID}</span>, where <span class="zs-monospace">{ID}</span> is a placeholder for the zettel identifier (14 digits).</p>
...
```

You also can use the query parameter ''_part=[[PART|00001012920800]]'' to specify which parts of a zettel must be encoded.
In this case, its default value is ''content''.
```sh
# curl 'http://127.0.0.1:23123/v/00001012053500?_enc=html&_part=meta'

Changes to docs/manual/00001012053600.zettel.

1
2
3
4
5
6

7
8
9
10

11
12
13
14
15
16
17
1
2
3
4
5

6
7
8
9

10
11
12
13
14
15
16
17





-
+



-
+







id: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210830165056
modified: 20211103164337

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

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

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/p/00001012053600
{"meta":{"title":[{"t":"Text","s":"Retrieve"},{"t":"Space"},{"t":"Text","s":"parsed"},{"t":"Space"},{"t":"Text","s":"metadata"},{"t":"Space"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"content"},{"t":"Space"},{"t":"Text","s":"of"},{"t":"Space"},{"t":"Text","s":"an"},{"t":"Space"},{"t":"Text","s":"existing"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"in"},{"t":"Space"},{"t":"Text","s":"various"},{"t":"Space"},{"t":"Text","s":"encodings"}],"role":"manual","tags":["#api", ...

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

6
7
8
9

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: 00001012053800
title: API: Retrieve context of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210825194347
modified: 20211103163027

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

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

````
# curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2'
{"id": "00001012053800","meta": {...},"list": [{"id": "00001012921000","meta": {...}},{"id": "00001012920800","meta": {...}},{"id": "00010000000000","meta": {...}}]}
````

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

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

24
25
26
27
28
29
30
31





-
+

















-
+







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

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 ''/j/{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/j/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.
However, the value should form a valid URL that could be used later to [[read the content|00001012053300]] of the freshly renamed zettel.

[!plain]Alternatively, you can also use the [[endpoint|00001012920000]] ''/z/{ID}''.
Both endpoints behave identical.

=== HTTP Status codes
; ''204''
: Rename was successful, there is no body in the response.

Added docs/manual/00001012070500.zettel.

























































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012070500
title: API: Encode Zettelmarkup inline material as HTML/Text
role: zettel
tags: #api #manual #zettelstore
syntax: zmk
modified: 20211111145202

To encode [[Zettelmarkup inline material|00001007040000]] send a HTTP POST request to the [[endpoint|00001012920000]] ''/v''.
The POST body must contain a JSON encoded list of Zettelmarkup inline material to be encoded:

; ''first-zmk''
: Contains the first Zettelmarkup encoded material.
  This will be encoded as [[HTML|00001012920510]] and [[Text|00001012920519]].
; ''other-zmk''
: Contain more material.
  The list can be empty.
  These will be encoded in HTML only.
; ''lang''
: Specifies the language for HTML encoding.
  If empty, the default language of the Zettelstore instance will be used.
; ''no-links''
: A boolean value, which specifies whether links should be encoded (``"no-links":false``) or should be not encoded (``"no-links":true``).
  Default: ''false''.

Typically, this call will be used to encode the [[title|00001006020000#title]] of a zettel.

If successful, the call will return the following JSON document:

; ''first-html''
: HTML encoding of ''first-zmk''
; ''first-text''
: Text encoding of ''first-zmk''
; ''other_html''
: HTML encoding of the corresponding value in ''other-zmk''.

Encoding takes place in the context of all other zettel in the Zettelstore.
For example, links and images are evaluated according to this context.

A simple example:
```sh
# curl -X POST --data '{"first-zmk":"hallo [[00000000000001]]"}' http://127.0.0.1:23123/v
{"first-html":"hallo <a href=\"00000000000001\">00000000000001</a>","first-text":"hallo ","other-html":null}

# curl -X POST --data '{"first-zmk":"hallo [[00000000000001]]","no-links":true}' http://127.0.0.1:23123/v
{"first-html":"hallo <span>00000000000001</span>","first-text":"hallo ","other-html":null}
```

=== HTTP Status codes
; ''200''
: Operation was successful, the body contains a JSON object as described above.
; ''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 perform this operation.

Changes to docs/manual/00001012920000.zettel.

1
2
3
4
5
6

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

22
23
24
25

26
27
28
29
30

31
32

33
34
35
36
37
38
39
40
1
2
3
4
5

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

21
22
23
24
25
26
27
28
29
30

31
32

33
34
35
36
37
38
39
40
41





-
+














-
+




+




-
+

-
+








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

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
| ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate
|       | PUT: [[renew access token|00001012050400]] |
| ''j'' | GET: [[list zettel AS JSON|00001012051200]] | GET: [[retrieve zettel AS JSON|00001012053400]] | **J**SON
| ''j'' | GET: [[list zettel AS JSON|00001012051200]] | GET: [[retrieve zettel AS JSON|00001012053300]] | **J**SON
|       | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]]
|       |  | DELETE: [[delete the zettel|00001012054600]]
|       |  | MOVE: [[rename the zettel|00001012054400]]
| ''l'' |  | GET: [[list references|00001012053700]] | **L**inks
| ''m'' |  | GET: [[retrieve metadata|00001012053400]] | **M**etadata
| ''o'' |  | GET: [[list zettel order|00001012054000]] | **O**rder
| ''p'' |  | GET: [[retrieve parsed zettel|00001012053600]]| **P**arsed
| ''r'' | GET: [[list roles|00001012052600]] | | **R**oles
| ''t'' | GET: [[list tags|00001012052400]] || **T**ags
| ''v'' |  | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated
| ''v'' | POST: [[encode inlines|00001012070500]] | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated
| ''x'' |  | GET: [[list zettel context|00001012053800]] | Conte**x**t
| ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053400#plain]] | **Z**ettel
| ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053300#plain]] | **Z**ettel
|       | POST: [[create new zettel|00001012053200#plain]] | PUT: [[update a zettel|00001012054200#plain]]
|       |  | DELETE: [[delete zettel|00001012054600#plain]]
|       |  | MOVE: [[rename zettel|00001012054400#plain]]

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

Added docs/manual/00001018000000.zettel.


















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001018000000
title: Troubleshooting
role: zettel
tags: #manual #zettelstore
syntax: zmk
modified: 20211027125603

This page lists some problems and their solutions that may occur when using your Zettelstore.

=== Authentication
* **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner.
  But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message.
** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http:/\/'', and not ''http**s**:/\/'', for example ''http:/\/localhost:23123''.
   The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http:/\/'' schema.
   To be secure by default, the Zettelstore will not work in an insecure environment.
** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file.
** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''http**s**:/\/'' schema.

Changes to domain/content.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
8
9
10
11
12
13
14
15
16
17
18


19
20

21
22
23
24

25
26
27
28
29


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



46
47
48
49
50
51
52

53
54
55
56

57
58
59

60
61
62
63
64
65
66
67
68
69
70

71
72
73
74
75
76

77
78

79
80
81
82
83
84
85

86
87
88
89
90
91

92
93
94
95
96
97
98
99


100
101
102
103

104
105

106
107
108
109
110
111







+



-
-
+
+
-




-
+




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



-
-
-
+
+
+




-
+



-
+


-
+










-
+





-
+

-
+






-
+





-
+







-
-
+
+


-
+

-
+





// under this license.
//-----------------------------------------------------------------------------

// Package domain provides domain specific types, constants, and functions.
package domain

import (
	"bytes"
	"encoding/base64"
	"errors"
	"io"
	"unicode/utf8"

	"unicode"
	"unicode/utf8"
	"zettelstore.de/z/strfun"
)

// Content is just the content of a zettel.
type Content struct {
	data     string
	data     []byte
	isBinary bool
}

// NewContent creates a new content from a string.
func NewContent(s string) Content {
	return Content{data: s, isBinary: calcIsBinary(s)}
func NewContent(data []byte) Content {
	return Content{data: data, isBinary: calcIsBinary(data)}
}

// Equal compares two content values.
func (zc *Content) Equal(o *Content) bool {
	if zc == nil {
		return o == nil
	}
	if zc.isBinary != o.isBinary {
		return false
	}
	return bytes.Equal(zc.data, o.data)
}

// Set content to new string value.
func (zc *Content) Set(s string) {
	zc.data = s
	zc.isBinary = calcIsBinary(s)
func (zc *Content) Set(data []byte) {
	zc.data = data
	zc.isBinary = calcIsBinary(data)
}

// Write it to a Writer
func (zc *Content) Write(w io.Writer) (int, error) {
	return io.WriteString(w, zc.data)
	return w.Write(zc.data)
}

// AsString returns the content itself is a string.
func (zc *Content) AsString() string { return zc.data }
func (zc *Content) AsString() string { return string(zc.data) }

// AsBytes returns the content itself is a byte slice.
func (zc *Content) AsBytes() []byte { return []byte(zc.data) }
func (zc *Content) AsBytes() []byte { return zc.data }

// IsBinary returns true if the content contains non-unicode values or is,
// interpreted a text, with a high probability binary content.
func (zc *Content) IsBinary() bool { return zc.isBinary }

// TrimSpace remove some space character in content, if it is not binary content.
func (zc *Content) TrimSpace() {
	if zc.isBinary {
		return
	}
	zc.data = strfun.TrimSpaceRight(zc.data)
	zc.data = bytes.TrimRightFunc(zc.data, unicode.IsSpace)
}

// Encode content for future transmission.
func (zc *Content) Encode() (data, encoding string) {
	if !zc.isBinary {
		return zc.data, ""
		return zc.AsString(), ""
	}
	return base64.StdEncoding.EncodeToString([]byte(zc.data)), "base64"
	return base64.StdEncoding.EncodeToString(zc.data), "base64"
}

// SetDecoded content to the decoded value of the given string.
func (zc *Content) SetDecoded(data, encoding string) error {
	switch encoding {
	case "":
		zc.data = data
		zc.data = []byte(data)
	case "base64":
		decoded, err := base64.StdEncoding.DecodeString(data)
		if err != nil {
			return err
		}
		zc.data = string(decoded)
		zc.data = decoded
	default:
		return errors.New("unknown encoding " + encoding)
	}
	zc.isBinary = calcIsBinary(zc.data)
	return nil
}

func calcIsBinary(s string) bool {
	if !utf8.ValidString(s) {
func calcIsBinary(data []byte) bool {
	if !utf8.Valid(data) {
		return true
	}
	l := len(s)
	l := len(data)
	for i := 0; i < l; i++ {
		if s[i] == 0 {
		if data[i] == 0 {
			return true
		}
	}
	return false
}

Changes to domain/content_test.go.

24
25
26
27
28
29
30
31

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

31
32
33
34
35
36
37







-
+






	}{
		{"abc", false},
		{"äöü", false},
		{"", false},
		{string([]byte{0}), true},
	}
	for i, tc := range td {
		content := domain.NewContent(tc.s)
		content := domain.NewContent([]byte(tc.s))
		got := content.IsBinary()
		if got != tc.exp {
			t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got)
		}
	}
}

Changes to domain/id/id.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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28



29
30


31
32


33



34
35
36





37
38















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











52




53
54
55
56
57
58
59
60







+
+









-
-
-


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







// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (
	"strconv"
	"time"

	"zettelstore.de/c/api"
)

// Zid is the internal identifier of a zettel. Typically, it is a
// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
// A zettelstore implementation should try to set the last two digits to zero,
// e.g. the seconds should be zero,
type Zid uint64

// Some important ZettelIDs.
// Note: if you change some values, ensure that you also change them in the
//       constant box. They are mentioned there literally, because these
//       constants are not available there.
const (
	Invalid = Zid(0) // Invalid is a Zid that will never be valid

	// System zettel
)

	VersionZid              = Zid(1)
	HostZid                 = Zid(2)
// ZettelIDs that are used as Zid more than once.
	OperatingSystemZid      = Zid(3)
	LicenseZid              = Zid(4)
	AuthorsZid              = Zid(5)
// Note: if you change some values, ensure that you also change them in the
//       constant box. They are mentioned there literally, because these
//       constants are not available there.
	DependenciesZid         = Zid(6)
	BoxManagerZid           = Zid(20)
	MetadataKeyZid          = Zid(90)
	StartupConfigurationZid = Zid(96)
	ConfigurationZid        = Zid(100)
var (
	ConfigurationZid   = MustParse(api.ZidConfiguration)

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

	BaseTemplateZid    = MustParse(api.ZidBaseTemplate)
	LoginTemplateZid   = MustParse(api.ZidLoginTemplate)
	ListTemplateZid    = MustParse(api.ZidListTemplate)
	ZettelTemplateZid  = MustParse(api.ZidZettelTemplate)
	InfoTemplateZid    = MustParse(api.ZidInfoTemplate)
	FormTemplateZid    = MustParse(api.ZidFormTemplate)
	RenameTemplateZid  = MustParse(api.ZidRenameTemplate)
	DeleteTemplateZid  = MustParse(api.ZidDeleteTemplate)
	ContextTemplateZid = MustParse(api.ZidContextTemplate)
	RolesTemplateZid   = MustParse(api.ZidRolesTemplate)
	TagsTemplateZid    = MustParse(api.ZidTagsTemplate)
	ErrorTemplateZid   = MustParse(api.ZidErrorTemplate)
	EmojiZid           = MustParse(api.ZidEmoji)
	// 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
	TOCNewTemplateZid    = Zid(90000)
	TOCNewTemplateZid  = MustParse(api.ZidTOCNewTemplate)
	TemplateNewZettelZid = Zid(90001)
	TemplateNewUserZid   = Zid(90002)

	DefaultHomeZid = Zid(10000000000)
	DefaultHomeZid     = MustParse(api.ZidDefaultHome)
)

const maxZid = 99999999999999

// ParseUint interprets a string as a possible zettel identifier
// and returns its integer value.
func ParseUint(s string) (uint64, error) {
96
97
98
99
100
101
102










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

115
116
117
118


119
120
121
122
123
124
125
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107


108
109
110
111
112
113
114
115
116







+
+
+
+
+
+
+
+
+
+












+


-
-
+
+







	}
	res, err := ParseUint(s)
	if err != nil {
		return Invalid, err
	}
	return Zid(res), nil
}

// MustParse tries to interpret a string as a zettel identifier and returns
// its value or panics otherwise.
func MustParse(s api.ZettelID) Zid {
	zid, err := Parse(string(s))
	if err == nil {
		return zid
	}
	panic(err)
}

const digits = "0123456789"

// String converts the zettel identification to a string of 14 digits.
// Only defined for valid ids.
func (zid Zid) String() string {
	return string(zid.Bytes())
}

// Bytes converts the zettel identification to a byte slice of 14 digits.
// Only defined for valid ids.
func (zid Zid) Bytes() []byte {
	n := uint64(zid)
	result := make([]byte, 14)
	for i := 13; i >= 0; i-- {
		result[i] = digits[zid%10]
		zid /= 10
		result[i] = digits[n%10]
		n /= 10
	}
	return result
}

// IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits.
func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid }

Changes to domain/id/id_test.go.

43
44
45
46
47
48
49
50

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

50
51
52
53
54
55
56
57







-
+







		zid, err := id.Parse(sid)
		if err != nil {
			t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err)
		}
		s := zid.String()
		if s != sid {
			t.Errorf(
				"i=%d: zid=%v does not format to %q, but to %q", i, sid, zid, s)
				"i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s)
		}
	}

	invalidIDs := []string{
		"", "0", "a",
		"00000000000000",
		"0000000000000a",

Changes to domain/id/slice.go.

9
10
11
12
13
14
15

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

18
19
20
21
22
23
24







+

-







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

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (
	"bytes"
	"sort"
	"strings"
)

// Slice is a sequence of zettel identifier. A special case is a sorted slice.
type Slice []Zid

func (zs Slice) Len() int           { return len(zs) }
func (zs Slice) Less(i, j int) bool { return zs[i] < zs[j] }
54
55
56
57
58
59
60
61

62
63
64

65
66

67
68

69
54
55
56
57
58
59
60

61
62
63

64
65

66
67

68
69







-
+


-
+

-
+

-
+

	return true
}

func (zs Slice) String() string {
	if len(zs) == 0 {
		return ""
	}
	var sb strings.Builder
	var buf bytes.Buffer
	for i, zid := range zs {
		if i > 0 {
			sb.WriteByte(' ')
			buf.WriteByte(' ')
		}
		sb.WriteString(zid.String())
		buf.WriteString(zid.String())
	}
	return sb.String()
	return buf.String()
}

Changes to domain/meta/meta.go.

8
9
10
11
12
13
14

15
16
17


18

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







+



+
+

+







// under this license.
//-----------------------------------------------------------------------------

// Package meta provides the domain specific type 'meta'.
package meta

import (
	"bytes"
	"regexp"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

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

type keyUsage int

const (
41
42
43
44
45
46
47
48

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

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

72
73
74
75
76
77
78







-
+



















-







func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed }

// IsProperty returns true, if metadata is a computed property.
func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty }

var registeredKeys = make(map[string]*DescriptionKey)

func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) string {
func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) {
	if _, ok := registeredKeys[name]; ok {
		panic("Key '" + name + "' already defined")
	}
	if inverse != "" {
		if t != TypeID && t != TypeIDSet {
			panic("Inversable key '" + name + "' is not identifier type, but " + t.String())
		}
		inv, ok := registeredKeys[inverse]
		if !ok {
			panic("Inverse Key '" + inverse + "' not found")
		}
		if !inv.IsComputed() {
			panic("Inverse Key '" + inverse + "' is not computed.")
		}
		if inv.Type != TypeIDSet {
			panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String())
		}
	}
	registeredKeys[name] = &DescriptionKey{name, t, usage, inverse}
	return name
}

// IsComputed returns true, if key denotes a computed metadata key.
func IsComputed(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsComputed()
	}
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141







































142
143
144
145
146
147
148
149
150
151







152

153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
106
107
108
109
110
111
112
































113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151










152
153
154
155
156
157
158

159
160
161
162
163























164
165
166
167
168
169
170







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




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







	for _, n := range names {
		result = append(result, registeredKeys[n])
	}
	return result
}

// Supported keys.
var (
	KeyID                = registerKey("id", TypeID, usageComputed, "")
	KeyTitle             = registerKey("title", TypeZettelmarkup, usageUser, "")
	KeyRole              = registerKey("role", TypeWord, usageUser, "")
	KeyTags              = registerKey("tags", TypeTagSet, usageUser, "")
	KeySyntax            = registerKey("syntax", TypeWord, usageUser, "")
	KeyAllTags           = registerKey("all-"+KeyTags, TypeTagSet, usageProperty, "")
	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, "")
	KeyMarkerExternal    = registerKey("marker-external", TypeEmpty, usageUser, "")
	KeyMaxTransclusions  = registerKey("max-transclusions", TypeNumber, usageUser, "")
	KeyModified          = registerKey("modified", TypeTimestamp, usageComputed, "")
	KeyNoIndex           = registerKey("no-index", TypeBool, usageUser, "")
func init() {
	registerKey(api.KeyID, TypeID, usageComputed, "")
	registerKey(api.KeyTitle, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeyRole, TypeWord, usageUser, "")
	registerKey(api.KeyTags, TypeTagSet, usageUser, "")
	registerKey(api.KeySyntax, TypeWord, usageUser, "")
	registerKey(api.KeyAllTags, TypeTagSet, usageProperty, "")
	registerKey(api.KeyBack, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBackward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBoxNumber, TypeNumber, usageComputed, "")
	registerKey(api.KeyContentTags, TypeTagSet, usageProperty, "")
	registerKey(api.KeyCopyright, TypeString, usageUser, "")
	registerKey(api.KeyCredential, TypeCredential, usageUser, "")
	registerKey(api.KeyDead, TypeIDSet, usageProperty, "")
	registerKey(api.KeyDuplicates, TypeBool, usageProperty, "")
	registerKey(api.KeyFolge, TypeIDSet, usageProperty, "")
	registerKey(api.KeyForward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyLang, TypeWord, usageUser, "")
	registerKey(api.KeyLicense, TypeEmpty, usageUser, "")
	registerKey(api.KeyModified, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyNoIndex, TypeBool, usageUser, "")
	registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge)
	registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "")
	registerKey(api.KeyReadOnly, TypeWord, usageUser, "")
	registerKey(api.KeyURL, TypeURL, usageUser, "")
	registerKey(api.KeyUserID, TypeWord, usageUser, "")
	registerKey(api.KeyUserRole, TypeWord, usageUser, "")
	registerKey(api.KeyVisibility, TypeWord, usageUser, "")

	// Keys for runtime configuration zettel
	// See: https://zettelstore.de/manual/h/00001004020000
	registerKey(api.KeyDefaultCopyright, TypeString, usageUser, "")
	registerKey(api.KeyDefaultLang, TypeWord, usageUser, "")
	registerKey(api.KeyDefaultLicense, TypeEmpty, usageUser, "")
	registerKey(api.KeyDefaultRole, TypeWord, usageUser, "")
	registerKey(api.KeyDefaultSyntax, TypeWord, usageUser, "")
	registerKey(api.KeyDefaultTitle, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeyDefaultVisibility, TypeWord, usageUser, "")
	registerKey(api.KeyExpertMode, 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, "")
	registerKey(api.KeyFooterHTML, TypeString, usageUser, "")
	registerKey(api.KeyHomeZettel, TypeID, usageUser, "")
	registerKey(api.KeyMarkerExternal, TypeEmpty, usageUser, "")
	registerKey(api.KeyMaxTransclusions, TypeNumber, usageUser, "")
	registerKey(api.KeySiteName, TypeString, usageUser, "")
	registerKey(api.KeyYAMLHeader, TypeBool, usageUser, "")
	registerKey(api.KeyZettelFileSyntax, 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.
type Meta struct {
	Zid     id.Zid
	pairs   map[string]string
	YamlSep bool
}

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







-
+











-
+











-
+








// Pair is one key-value-pair of a Zettel meta.
type Pair struct {
	Key   string
	Value string
}

var firstKeys = []string{KeyTitle, KeyRole, KeyTags, KeySyntax}
var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax}
var firstKeySet map[string]bool

func init() {
	firstKeySet = make(map[string]bool, len(firstKeys))
	for _, k := range firstKeys {
		firstKeySet[k] = true
	}
}

// Set stores the given string value under the given key.
func (m *Meta) Set(key, value string) {
	if key != KeyID {
	if key != api.KeyID {
		m.pairs[key] = trimValue(value)
	}
}

func trimValue(value string) string {
	return strings.TrimFunc(value, input.IsSpace)
}

// Get retrieves the string value of a given key. The bool value signals,
// whether there was a value stored or not.
func (m *Meta) Get(key string) (string, bool) {
	if key == KeyID {
	if key == api.KeyID {
		return m.Zid.String(), true
	}
	value, ok := m.pairs[key]
	return value, ok
}

// GetDefault retrieves the string value of the given key. If no value was
304
305
306
307
308
309
310
311

312
313
314
315
316
317
318
288
289
290
291
292
293
294

295
296
297
298
299
300
301
302







-
+







		result = append(result, Pair{k, m.pairs[k]})
	}
	return result
}

// Delete removes a key from the data.
func (m *Meta) Delete(key string) {
	if key != KeyID {
	if key != api.KeyID {
		delete(m.pairs, key)
	}
}

// Equal compares to metas for equality.
func (m *Meta) Equal(o *Meta, allowComputed bool) bool {
	if m == nil && o == nil {
340
341
342
343
344
345
346



































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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
	if allowComputed || !IsComputed(key) {
		if valO, ok := other.pairs[key]; !ok || val != valO {
			return false
		}
	}
	return true
}

// Sanitize all metadata keys and values, so that they can be written safely into a file.
func (m *Meta) Sanitize() {
	if m == nil {
		return
	}
	for k, v := range m.pairs {
		m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v)
	}
}

// RemoveNonGraphic changes the given string not to include non-graphical characters.
// It is needed to sanitize meta data.
func RemoveNonGraphic(s string) string {
	if s == "" {
		return ""
	}
	pos := 0
	var buf bytes.Buffer
	for pos < len(s) {
		nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) })
		if nextPos < 0 {
			break
		}
		buf.WriteString(s[pos:nextPos])
		buf.WriteByte(' ')
		_, size := utf8.DecodeRuneInString(s[nextPos:])
		pos = nextPos + size
	}
	if pos == 0 {
		return strings.TrimSpace(s)
	}
	buf.WriteString(s[pos:])
	return strings.TrimSpace(buf.String())
}

Changes to domain/meta/meta_test.go.

11
12
13
14
15
16
17

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







+







// Package meta provides the domain specific type 'meta'.
package meta

import (
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	t.Parallel()
35
36
37
38
39
40
41
42

43
44
45
46


47
48
49
50
51


52
53
54

55
56

57
58
59
60
61
62
63
64



65
66
67
68
69
70
71
36
37
38
39
40
41
42

43
44
45


46
47
48
49
50


51
52
53
54

55
56

57
58
59
60
61
62



63
64
65
66
67
68
69
70
71
72







-
+


-
-
+
+



-
-
+
+


-
+

-
+





-
-
-
+
+
+







		}
	}
}

func TestTitleHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(KeyTitle); ok || got != "" {
	if got, ok := m.Get(api.KeyTitle); ok || got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	addToMeta(m, KeyTitle, " ")
	if got, ok := m.Get(KeyTitle); ok || got != "" {
	addToMeta(m, api.KeyTitle, " ")
	if got, ok := m.Get(api.KeyTitle); ok || got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	const st = "A simple text"
	addToMeta(m, KeyTitle, " "+st+"  ")
	if got, ok := m.Get(KeyTitle); !ok || got != st {
	addToMeta(m, api.KeyTitle, " "+st+"  ")
	if got, ok := m.Get(api.KeyTitle); !ok || got != st {
		t.Errorf("Title is not %q, but %q", st, got)
	}
	addToMeta(m, KeyTitle, "  "+st+"\t")
	addToMeta(m, api.KeyTitle, "  "+st+"\t")
	const exp = st + " " + st
	if got, ok := m.Get(KeyTitle); !ok || got != exp {
	if got, ok := m.Get(api.KeyTitle); !ok || got != exp {
		t.Errorf("Title is not %q, but %q", exp, got)
	}

	m = New(testID)
	const at = "A Title"
	addToMeta(m, KeyTitle, at)
	addToMeta(m, KeyTitle, " ")
	if got, ok := m.Get(KeyTitle); !ok || got != at {
	addToMeta(m, api.KeyTitle, at)
	addToMeta(m, api.KeyTitle, " ")
	if got, ok := m.Get(api.KeyTitle); !ok || got != at {
		t.Errorf("Title is not %q, but %q", at, got)
	}
}

func checkSet(t *testing.T, exp []string, m *Meta, key string) {
	t.Helper()
	got, _ := m.GetList(key)
82
83
84
85
86
87
88
89

90
91
92


93
94
95


96
97
98


99
100
101


102
103
104
105
106
107

108
109
110
111


112
113
114

115
116

117
118
119
120


121
122
123
124
125
126
127
83
84
85
86
87
88
89

90
91


92
93
94


95
96
97


98
99
100


101
102
103
104
105
106
107

108
109
110


111
112
113
114

115
116

117
118
119


120
121
122
123
124
125
126
127
128







-
+

-
-
+
+

-
-
+
+

-
-
+
+

-
-
+
+





-
+


-
-
+
+


-
+

-
+


-
-
+
+







		t.Errorf("Extra tags: %q", got[len(exp):])
	}
}

func TestTagsHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	checkSet(t, []string{}, m, KeyTags)
	checkSet(t, []string{}, m, api.KeyTags)

	addToMeta(m, KeyTags, "")
	checkSet(t, []string{}, m, KeyTags)
	addToMeta(m, api.KeyTags, "")
	checkSet(t, []string{}, m, api.KeyTags)

	addToMeta(m, KeyTags, "  #t1 #t2  #t3 #t4  ")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags)
	addToMeta(m, api.KeyTags, "  #t1 #t2  #t3 #t4  ")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, api.KeyTags)

	addToMeta(m, KeyTags, "#t5")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags)
	addToMeta(m, api.KeyTags, "#t5")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags)

	addToMeta(m, KeyTags, "t6")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags)
	addToMeta(m, api.KeyTags, "t6")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags)
}

func TestSyntax(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(KeySyntax); ok || got != "" {
	if got, ok := m.Get(api.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, KeySyntax, " ")
	if got, _ := m.Get(KeySyntax); got != "" {
	addToMeta(m, api.KeySyntax, " ")
	if got, _ := m.Get(api.KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, KeySyntax, "MarkDown")
	addToMeta(m, api.KeySyntax, "MarkDown")
	const exp = "markdown"
	if got, ok := m.Get(KeySyntax); !ok || got != exp {
	if got, ok := m.Get(api.KeySyntax); !ok || got != exp {
		t.Errorf("Syntax is not %q, but %q", exp, got)
	}
	addToMeta(m, KeySyntax, " ")
	if got, _ := m.Get(KeySyntax); got != "" {
	addToMeta(m, api.KeySyntax, " ")
	if got, _ := m.Get(api.KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
}

func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) {
	t.Helper()
	got := make(map[string]string, len(gotP))
186
187
188
189
190
191
192
193
194
195
196




197
198
199
200
201
202
203
187
188
189
190
191
192
193




194
195
196
197
198
199
200
201
202
203
204







-
-
-
-
+
+
+
+







		allowComputed  bool
		exp            bool
	}{
		{nil, nil, true, true},
		{nil, nil, false, true},
		{[]string{"a", "a"}, nil, false, false},
		{[]string{"a", "a"}, nil, true, false},
		{[]string{KeyFolge, "0"}, nil, true, false},
		{[]string{KeyFolge, "0"}, nil, false, true},
		{[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, true, true},
		{[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, false, true},
		{[]string{api.KeyFolge, "0"}, nil, true, false},
		{[]string{api.KeyFolge, "0"}, nil, false, true},
		{[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true},
		{[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true},
	}
	for i, tc := range testcases {
		m1 := pairs2meta(tc.pairs1)
		m2 := pairs2meta(tc.pairs2)
		got := m1.Equal(m2, tc.allowComputed)
		if tc.exp != got {
			t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
232
233
234
235
236
237
238























233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
func pairs2meta(pairs []string) *Meta {
	m := New(testID)
	for i := 0; i < len(pairs); i = i + 2 {
		m.Set(pairs[i], pairs[i+1])
	}
	return m
}

func TestRemoveNonGraphic(t *testing.T) {
	testCases := []struct {
		inp string
		exp string
	}{
		{"", ""},
		{" ", ""},
		{"a", "a"},
		{"a ", "a"},
		{"a b", "a b"},
		{"\n", ""},
		{"a\n", "a"},
		{"a\nb", "a b"},
		{"a\tb", "a b"},
	}
	for i, tc := range testCases {
		got := RemoveNonGraphic(tc.inp)
		if tc.exp != got {
			t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got)
		}
	}
}

Changes to domain/meta/parse.go.

11
12
13
14
15
16
17

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







+







// Package meta provides the domain specific type 'meta'.
package meta

import (
	"sort"
	"strings"

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

// 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) == '-' {
60
61
62
63
64
65
66
67

68
69
70
71
72

73
74
75
76
77

78
79

80
81
82
83
84
85
86
61
62
63
64
65
66
67

68
69
70
71
72

73
74
75
76
77

78
79

80
81
82
83
84
85
86
87







-
+




-
+




-
+

-
+







		inp.Next()
	}
	key := inp.Src[pos:inp.Pos]
	skipSpace(inp)
	if inp.Ch == ':' {
		inp.Next()
	}
	var val string
	var val []byte
	for {
		skipSpace(inp)
		pos = inp.Pos
		skipToEOL(inp)
		val += inp.Src[pos:inp.Pos]
		val = append(val, inp.Src[pos:inp.Pos]...)
		inp.EatEOL()
		if !input.IsSpace(inp.Ch) {
			break
		}
		val += " "
		val = append(val, ' ')
	}
	addToMeta(m, key, val)
	addToMeta(m, string(key), string(val))
}

func skipSpace(inp *input.Input) {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	}
}
147
148
149
150
151
152
153
154

155
156
157
158
159
160
161
148
149
150
151
152
153
154

155
156
157
158
159
160
161
162







-
+







func addToMeta(m *Meta, key, val string) {
	v := trimValue(val)
	key = strings.ToLower(key)
	if !KeyIsValid(key) {
		return
	}
	switch key {
	case "", KeyID:
	case "", api.KeyID:
		// Empty key and 'id' key will be ignored
		return
	}

	switch Type(key) {
	case TypeString, TypeZettelmarkup:
		if v != "" {

Changes to domain/meta/parse_test.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
10
11
12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28

29
30
31

32
33
34
35
36
37
38
39











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

54
55
56
57
58
59


60
61
62
63
64
65
66
67
68







+





-
+





-
+


-
+







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



-
+





-
-
+
+








// Package meta_test provides tests for the domain specific type 'meta'.
package meta_test

import (
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
)

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput(src))
	return meta.NewFromInput(testID, input.NewInput([]byte(src)))
}

func TestEmpty(t *testing.T) {
	t.Parallel()
	m := parseMetaStr("")
	if got, ok := m.Get(meta.KeySyntax); ok || got != "" {
	if got, ok := m.Get(api.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 {
	if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 {
		t.Errorf("Tags are not nil, but %v", got)
	}
}

func TestTitle(t *testing.T) {
	t.Parallel()
	td := []struct{ s, e string }{
		{meta.KeyTitle + ": a title", "a title"},
		{meta.KeyTitle + ": a\n\t title", "a title"},
		{meta.KeyTitle + ": a\n\t title\r\n  x", "a title x"},
		{meta.KeyTitle + " AbC", "AbC"},
		{meta.KeyTitle + " AbC\n ded", "AbC ded"},
		{meta.KeyTitle + ": o\ntitle: p", "o p"},
		{meta.KeyTitle + ": O\n\ntitle: P", "O"},
		{meta.KeyTitle + ": b\r\ntitle: c", "b c"},
		{meta.KeyTitle + ": B\r\n\r\ntitle: C", "B"},
		{meta.KeyTitle + ": r\rtitle: q", "r q"},
		{meta.KeyTitle + ": R\r\rtitle: Q", "R"},
		{api.KeyTitle + ": a title", "a title"},
		{api.KeyTitle + ": a\n\t title", "a title"},
		{api.KeyTitle + ": a\n\t title\r\n  x", "a title x"},
		{api.KeyTitle + " AbC", "AbC"},
		{api.KeyTitle + " AbC\n ded", "AbC ded"},
		{api.KeyTitle + ": o\ntitle: p", "o p"},
		{api.KeyTitle + ": O\n\ntitle: P", "O"},
		{api.KeyTitle + ": b\r\ntitle: c", "b c"},
		{api.KeyTitle + ": B\r\n\r\ntitle: C", "B"},
		{api.KeyTitle + ": r\rtitle: q", "r q"},
		{api.KeyTitle + ": R\r\rtitle: Q", "R"},
	}
	for i, tc := range td {
		m := parseMetaStr(tc.s)
		if got, ok := m.Get(meta.KeyTitle); !ok || got != tc.e {
		if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e {
			t.Log(m)
			t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got)
		}
	}

	m := parseMetaStr(meta.KeyTitle + ": ")
	if title, ok := m.Get(meta.KeyTitle); ok {
	m := parseMetaStr(api.KeyTitle + ": ")
	if title, ok := m.Get(api.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 {
83
84
85
86
87
88
89
90

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

91
92
93
94
95
96
97
98







-
+







		meta := parseMetaStr(tc.input)
		if got := meta.Pairs(true); !equalPairs(tc.exp, got) {
			t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got)
		}
	}

	// Test, whether input position is correct.
	inp := input.NewInput("---\na:b\n---\nX")
	inp := input.NewInput([]byte("---\na:b\n---\nX"))
	m := meta.NewFromInput(testID, inp)
	exp := []meta.Pair{{"a", "b"}}
	if got := m.Pairs(true); !equalPairs(exp, got) {
		t.Errorf("Expected=%v, got=%v", exp, got)
	}
	expCh := 'X'
	if gotCh := inp.Ch; gotCh != expCh {
122
123
124
125
126
127
128
129
130


131
132
133
134
123
124
125
126
127
128
129


130
131
132
133
134
135







-
-
+
+




		{"12345678901234", "12345678901234"},
		{"123 12345678901234", "12345678901234"},
		{"12345678901234 123", "12345678901234"},
		{"01234567890123 123 12345678901234", "01234567890123 12345678901234"},
		{"12345678901234 01234567890123", "01234567890123 12345678901234"},
	}
	for i, tc := range testdata {
		m := parseMetaStr(meta.KeyPrecursor + ": " + tc.inp)
		if got, ok := m.Get(meta.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got {
		m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp)
		if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got {
			t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp)
		}
	}
}

Changes to domain/meta/type.go.

12
13
14
15
16
17
18


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







+
+







package meta

import (
	"strconv"
	"strings"
	"sync"
	"time"

	"zettelstore.de/c/api"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
}
93
94
95
96
97
98
99
100

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

102
103
104
105
106
107
108
109







-
+







		}
	}
	return TypeEmpty
}

// SetList stores the given string list value under the given key.
func (m *Meta) SetList(key string, values []string) {
	if key != KeyID {
	if key != api.KeyID {
		for i, val := range values {
			values[i] = trimValue(val)
		}
		m.pairs[key] = strings.Join(values, " ")
	}
}

Changes to domain/meta/type_test.go.

30
31
32
33
34
35
36
37

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

37
38
39
40
41
42
43
44







-
+







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

Changes to domain/meta/values.go.

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


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





34
35
36
37
38
39
40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 meta provides the domain specific type 'meta'.
package meta

import "zettelstore.de/c/api"

// Visibility enumerates the variations of the 'visibility' meta key.
type Visibility int

// Supported values for visibility.
const (
	_ Visibility = iota
	VisibilityUnknown
	VisibilityPublic
	VisibilityCreator
	VisibilityLogin
	VisibilityOwner
	VisibilityExpert
)

var visMap = map[string]Visibility{
	ValueVisibilityPublic:  VisibilityPublic,
	ValueVisibilityCreator: VisibilityCreator,
	ValueVisibilityLogin:   VisibilityLogin,
	ValueVisibilityOwner:   VisibilityOwner,
	ValueVisibilityExpert:  VisibilityExpert,
	api.ValueVisibilityPublic:  VisibilityPublic,
	api.ValueVisibilityCreator: VisibilityCreator,
	api.ValueVisibilityLogin:   VisibilityLogin,
	api.ValueVisibilityOwner:   VisibilityOwner,
	api.ValueVisibilityExpert:  VisibilityExpert,
}

// GetVisibility returns the visibility value of the given string
func GetVisibility(val string) Visibility {
	if vis, ok := visMap[val]; ok {
		return vis
	}
51
52
53
54
55
56
57
58
59
60
61




62
63
64
65
66
67
68
69
70
53
54
55
56
57
58
59




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







-
-
-
-
+
+
+
+









	UserRoleCreator
	UserRoleReader
	UserRoleWriter
	UserRoleOwner
)

var urMap = map[string]UserRole{
	ValueUserRoleCreator: UserRoleCreator,
	ValueUserRoleReader:  UserRoleReader,
	ValueUserRoleWriter:  UserRoleWriter,
	ValueUserRoleOwner:   UserRoleOwner,
	api.ValueUserRoleCreator: UserRoleCreator,
	api.ValueUserRoleReader:  UserRoleReader,
	api.ValueUserRoleWriter:  UserRoleWriter,
	api.ValueUserRoleOwner:   UserRoleOwner,
}

// GetUserRole role returns the user role of the given string.
func GetUserRole(val string) UserRole {
	if ur, ok := urMap[val]; ok {
		return ur
	}
	return UserRoleUnknown
}

Changes to domain/meta/write_test.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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

29
30
31

32
33
34

35
36
37
38
39
40



41
42
43
44
45
46
47
48
49
50







+



+









-
+


-
+


-
+





-
-
-
+
+
+







// under this license.
//-----------------------------------------------------------------------------

// Package meta_test provides tests for the domain specific type 'meta'.
package meta_test

import (
	"bytes"
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func newMeta(title string, tags []string, syntax string) *meta.Meta {
	m := meta.New(testID)
	if title != "" {
		m.Set(meta.KeyTitle, title)
		m.Set(api.KeyTitle, title)
	}
	if tags != nil {
		m.Set(meta.KeyTags, strings.Join(tags, " "))
		m.Set(api.KeyTags, strings.Join(tags, " "))
	}
	if syntax != "" {
		m.Set(meta.KeySyntax, syntax)
		m.Set(api.KeySyntax, syntax)
	}
	return m
}
func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
	t.Helper()
	sb := strings.Builder{}
	m.Write(&sb, true)
	if got := sb.String(); got != expected {
	var buf bytes.Buffer
	m.Write(&buf, true)
	if got := buf.String(); got != expected {
		t.Errorf("\nExp: %q\ngot: %q", expected, got)
	}
}

func TestWriteMeta(t *testing.T) {
	t.Parallel()
	assertWriteMeta(t, newMeta("", nil, ""), "")

Changes to domain/zettel.go.

19
20
21
22
23
24
25
26

27
19
20
21
22
23
24
25

26
27







-
+

type Zettel struct {
	Meta    *meta.Meta // Some additional meta-data.
	Content Content    // The content of the zettel itself.
}

// Equal compares two zettel for equality.
func (z Zettel) Equal(o Zettel, allowComputed bool) bool {
	return z.Meta.Equal(o.Meta, allowComputed) && z.Content == o.Content
	return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content)
}

Changes to encoder/djsonenc/djsonenc.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 (
	"fmt"
	"io"
	"sort"
	"strconv"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/strfun"
)

func init() {
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
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







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







		v.b.WriteByte('"')
		v.b.WriteString(fragment)
		v.b.WriteByte('"')
	}
}

var mapFormatKind = map[ast.FormatKind]string{
	ast.FormatItalic:    "Italic",
	ast.FormatEmph:      "Emph",
	ast.FormatEmphDeprecated: "EmphD",
	ast.FormatEmph:           "Emph",
	ast.FormatBold:      "Bold",
	ast.FormatStrong:    "Strong",
	ast.FormatMonospace: "Mono",
	ast.FormatStrong:         "Strong",
	ast.FormatMonospace:      "Mono",
	ast.FormatStrike:    "Strikethrough",
	ast.FormatDelete:    "Delete",
	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",
	ast.FormatInsert:         "Insert",
	ast.FormatSuper:          "Super",
	ast.FormatSub:            "Sub",
	ast.FormatQuote:          "Quote",
	ast.FormatQuotation:      "Quotation",
	ast.FormatSmall:          "Small",
	ast.FormatSpan:           "Span",
}

var mapLiteralKind = map[ast.LiteralKind]string{
	ast.LiteralProg:    "Code",
	ast.LiteralKeyb:    "Input",
	ast.LiteralOutput:  "Output",
	ast.LiteralComment: "Comment",

Changes to encoder/encoder.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







-
+







package encoder

import (
	"errors"
	"fmt"
	"io"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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, EvalMetaFunc) (int, error)

Added encoder/encoder_blob_test.go.




























































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

package encoder_test

import (
	"testing"

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

	_ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser.
)

type blobTestCase struct {
	descr  string
	blob   []byte
	expect expectMap
}

var pngTestCases = []blobTestCase{
	{
		descr: "Minimal PNG",
		blob: []byte{
			0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
			0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b,
			0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00,
			0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
			0x42, 0x60, 0x82,
		},
		expect: expectMap{
			encoderDJSON:  `[{"t":"Blob","q":"PNG","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`,
			encoderHTML:   `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="PNG">`,
			encoderNative: `[BLOB "PNG" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="]`,
			encoderText:   "",
			encoderZmk:    `%% Unable to display BLOB with title 'PNG' and syntax 'png'.`,
		},
	},
}

func TestBlob(t *testing.T) {
	m := meta.New(id.Invalid)
	m.Set(api.KeyTitle, "PNG")
	for testNum, tc := range pngTestCases {
		inp := input.NewInput(tc.blob)
		pe := &peBlocks{bln: parser.ParseBlocks(inp, m, "png")}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, "???")
	}
}

Added encoder/encoder_block_test.go.



















































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package encoder_test

var tcsBlock = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing",
		zmk:   "",
		expect: expectMap{
			encoderDJSON:  `[]`,
			encoderHTML:   "",
			encoderNative: ``,
			encoderText:   "",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world",
		zmk:   "Hello, world",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Para","i":[{"t":"Text","s":"Hello,"},{"t":"Space"},{"t":"Text","s":"world"}]}]`,
			encoderHTML:   "<p>Hello, world</p>",
			encoderNative: `[Para Text "Hello,",Space,Text "world"]`,
			encoderText:   "Hello, world",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple block comment",
		zmk:   "%%%\nNo\nrender\n%%%",
		expect: expectMap{
			encoderDJSON:  `[{"t":"CommentBlock","l":["No","render"]}]`,
			encoderHTML:   ``,
			encoderNative: `[CommentBlock "No\nrender"]`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Rendered block comment",
		zmk:   "%%%{-}\nRender\n%%%",
		expect: expectMap{
			encoderDJSON:  `[{"t":"CommentBlock","a":{"-":""},"l":["Render"]}]`,
			encoderHTML:   "<!--\nRender\n-->",
			encoderNative: `[CommentBlock ("",[-]) "Render"]`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple Heading",
		zmk:   `=== Top`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Heading","n":1,"s":"top","i":[{"t":"Text","s":"Top"}]}]`,
			encoderHTML:   "<h2 id=\"top\">Top</h2>",
			encoderNative: `[Heading 1 #top Text "Top"]`,
			encoderText:   `Top`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Einfache Liste",
		zmk:   "* A\n* B\n* C",
		expect: expectMap{
			encoderDJSON: `[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"A"}]}],[{"t":"Para","i":[{"t":"Text","s":"B"}]}],[{"t":"Para","i":[{"t":"Text","s":"C"}]}]]}]`,
			encoderHTML:  "<ul>\n<li>A</li>\n<li>B</li>\n<li>C</li>\n</ul>",
			encoderNative: `[BulletList
 [[Para Text "A"]],
 [[Para Text "B"]],
 [[Para Text "C"]]]`,
			encoderText: "A\nB\nC",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Schachtelliste",
		zmk:   "* T1\n** T2\n* T3\n** T4\n* T5",
		expect: expectMap{
			encoderDJSON: `[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T1"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T2"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T3"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T4"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T5"}]}]]}]`,
			encoderHTML: `<ul>
<li>
<p>T1</p>
<ul>
<li>T2</li>
</ul>
</li>
<li>
<p>T3</p>
<ul>
<li>T4</li>
</ul>
</li>
<li>
<p>T5</p>
</li>
</ul>`,
			encoderNative: `[BulletList
 [[Para Text "T1"],
  [BulletList
   [[Para Text "T2"]]]],
 [[Para Text "T3"],
  [BulletList
   [[Para Text "T4"]]]],
 [[Para Text "T5"]]]`,
			encoderText: "T1\nT2\nT3\nT4\nT5",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Zwei Listen hintereinander",
		zmk:   "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2",
		expect: expectMap{
			encoderDJSON: `[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item1.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.3"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.2"}]}]]}]`,
			encoderHTML:  "<ul>\n<li>Item1.1</li>\n<li>Item1.2</li>\n<li>Item1.3</li>\n<li>Item2.1</li>\n<li>Item2.2</li>\n</ul>",
			encoderNative: `[BulletList
 [[Para Text "Item1.1"]],
 [[Para Text "Item1.2"]],
 [[Para Text "Item1.3"]],
 [[Para Text "Item2.1"]],
 [[Para Text "Item2.2"]]]`,
			encoderText: "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2",
			encoderZmk:  "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2",
		},
	},
	{
		descr: "Simple horizontal rule",
		zmk:   `---`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Hrule"}]`,
			encoderHTML:   "<hr>",
			encoderNative: `[Hrule]`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "No list after paragraph",
		zmk:   "Text\n*abc",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Soft"},{"t":"Text","s":"*abc"}]}]`,
			encoderHTML:   "<p>Text\n*abc</p>",
			encoderNative: `[Para Text "Text",Space,Text "*abc"]`,
			encoderText:   `Text *abc`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "A list after paragraph",
		zmk:   "Text\n* abc",
		expect: expectMap{
			encoderDJSON: `[{"t":"Para","i":[{"t":"Text","s":"Text"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"abc"}]}]]}]`,
			encoderHTML:  "<p>Text</p>\n<ul>\n<li>abc</li>\n</ul>",
			encoderNative: `[Para Text "Text"],
[BulletList
 [[Para Text "abc"]]]`,
			encoderText: "Text\nabc",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Simple Quote Block",
		zmk:   "<<<\nToBeOrNotToBe\n<<< Romeo",
		expect: expectMap{
			encoderDJSON: `[{"t":"QuoteBlock","b":[{"t":"Para","i":[{"t":"Text","s":"ToBeOrNotToBe"}]}],"i":[{"t":"Text","s":"Romeo"}]}]`,
			encoderHTML:  "<blockquote>\n<p>ToBeOrNotToBe</p>\n<cite>Romeo</cite>\n</blockquote>",
			encoderNative: `[QuoteBlock
 [[Para Text "ToBeOrNotToBe"]],
 [Cite Text "Romeo"]]`,
			encoderText: "ToBeOrNotToBe\nRomeo",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Verse block",
		zmk: `"""
A line
  another line
Back

Paragraph

    Spacy  Para
""" Author`,
		expect: expectMap{
			encoderDJSON:  "[{\"t\":\"VerseBlock\",\"b\":[{\"t\":\"Para\",\"i\":[{\"t\":\"Text\",\"s\":\"A\u00a0line\"},{\"t\":\"Hard\"},{\"t\":\"Text\",\"s\":\"\u00a0\u00a0another\u00a0line\"},{\"t\":\"Hard\"},{\"t\":\"Text\",\"s\":\"Back\"}]},{\"t\":\"Para\",\"i\":[{\"t\":\"Text\",\"s\":\"Paragraph\"}]},{\"t\":\"Para\",\"i\":[{\"t\":\"Text\",\"s\":\"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\"}]}],\"i\":[{\"t\":\"Text\",\"s\":\"Author\"}]}]",
			encoderHTML:   "<div>\n<p>A\u00a0line<br>\n\u00a0\u00a0another\u00a0line<br>\nBack</p>\n<p>Paragraph</p>\n<p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p>\n<cite>Author</cite>\n</div>",
			encoderNative: "[VerseBlock\n [[Para Text \"A\u00a0line\",Break,Text \"\u00a0\u00a0another\u00a0line\",Break,Text \"Back\"],\n  [Para Text \"Paragraph\"],\n  [Para Text \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\"]],\n [Cite Text \"Author\"]]",
			encoderText:   "A\u00a0line\n\u00a0\u00a0another\u00a0line\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\nAuthor",
			encoderZmk:    "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author",
		},
	},
	{
		descr: "Span Block",
		zmk: `:::
A simple
   span
and much more
:::`,
		expect: expectMap{
			encoderDJSON: `[{"t":"SpanBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Text","s":"simple"},{"t":"Soft"},{"t":"Space","n":3},{"t":"Text","s":"span"},{"t":"Soft"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"much"},{"t":"Space"},{"t":"Text","s":"more"}]}]}]`,
			encoderHTML:  "<div>\n<p>A simple\n span\nand much more</p>\n</div>",
			encoderNative: `[SpanBlock
 [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]]`,
			encoderText: `A simple  span and much more`,
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Simple Verbatim",
		zmk:   "```\nHello\nWorld\n```",
		expect: expectMap{
			encoderDJSON:  `[{"t":"CodeBlock","l":["Hello","World"]}]`,
			encoderHTML:   "<pre><code>Hello\nWorld\n</code></pre>",
			encoderNative: `[CodeBlock "Hello\nWorld"]`,
			encoderText:   "Hello\nWorld",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderDJSON: `[{"t":"DescriptionList","g":[[[{"t":"Text","s":"Zettel"}],[{"t":"Para","i":[{"t":"Text","s":"Paper"}]}],[{"t":"Para","i":[{"t":"Text","s":"Note"}]}]],[[{"t":"Text","s":"Zettelkasten"}],[{"t":"Para","i":[{"t":"Text","s":"Slip"},{"t":"Space"},{"t":"Text","s":"box"}]}]]]}]`,
			encoderHTML:  "<dl>\n<dt>Zettel</dt>\n<dd>Paper</dd>\n<dd>Note</dd>\n<dt>Zettelkasten</dt>\n<dd>Slip box</dd>\n</dl>",
			encoderNative: `[DescriptionList
 [Term [Text "Zettel"],
  [Description
   [Para Text "Paper"]],
  [Description
   [Para Text "Note"]]],
 [Term [Text "Zettelkasten"],
  [Description
   [Para Text "Slip",Space,Text "box"]]]]`,
			encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Simple Table",
		zmk:   "|c1|c2|c3\n|d1||d3",
		expect: expectMap{
			encoderDJSON: `[{"t":"Table","p":[[],[[["",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],["",[{"t":"Text","s":"c3"}]]],[["",[{"t":"Text","s":"d1"}]],["",[]],["",[{"t":"Text","s":"d3"}]]]]]}]`,
			encoderHTML: `<table>
<tbody>
<tr><td>c1</td><td>c2</td><td>c3</td></tr>
<tr><td>d1</td><td></td><td>d3</td></tr>
</tbody>
</table>`,
			encoderNative: `[Table
 [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]],
 [Row [Cell Default Text "d1"],[Cell Default],[Cell Default Text "d3"]]]`,
			encoderText: "c1 c2 c3\nd1  d3",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Table with alignment and comment",
		zmk: `|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3`,
		expect: expectMap{
			encoderDJSON: `[{"t":"Table","p":[[[">",[{"t":"Text","s":"h1"}]],["",[{"t":"Text","s":"h2"}]],[":",[{"t":"Text","s":"h3"}]]],[[["<",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],[":",[{"t":"Text","s":"c3"}]]],[[">",[{"t":"Text","s":"f1"}]],["",[{"t":"Text","s":"f2"}]],[":",[{"t":"Text","s":"=f3"}]]]]]}]`,
			encoderHTML: `<table>
<thead>
<tr><th class="zs-ta-right">h1</th><th>h2</th><th class="zs-ta-center">h3</th></tr>
</thead>
<tbody>
<tr><td class="zs-ta-left">c1</td><td>c2</td><td class="zs-ta-center">c3</td></tr>
<tr><td class="zs-ta-right">f1</td><td>f2</td><td class="zs-ta-center">=f3</td></tr>
</tbody>
</table>`,
			encoderNative: `[Table
 [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]],
 [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]],
 [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]]`,
			encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: `|=h1>|=h2|=h3:
|<c1|c2|c3
|f1|f2|=f3`,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderDJSON:  `[]`,
			encoderHTML:   ``,
			encoderNative: ``,
			encoderText:   "",
			encoderZmk:    useZmk,
		},
	},
}

// func TestEncoderBlock(t *testing.T) {
// 	executeTestCases(t, tcsBlock)
// }

Added encoder/encoder_inline_test.go.




























































































































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package encoder_test

var tcsInline = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing (inline)",
		zmk:   "",
		expect: expectMap{
			encoderDJSON:  `[]`,
			encoderHTML:   "",
			encoderNative: ``,
			encoderText:   "",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world (inline)",
		zmk:   `Hello, world`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Text","s":"Hello,"},{"t":"Space"},{"t":"Text","s":"world"}]`,
			encoderHTML:   "Hello, world",
			encoderNative: `Text "Hello,",Space,Text "world"`,
			encoderText:   "Hello, world",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Emphasized formatting",
		zmk:   "__emph__",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Emph","i":[{"t":"Text","s":"emph"}]}]`,
			encoderHTML:   "<em>emph</em>",
			encoderNative: `Emph [Text "emph"]`,
			encoderText:   "emph",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Emphasized formatting (deprecated)",
		zmk:   "//emphd//",
		expect: expectMap{
			encoderDJSON:  `[{"t":"EmphD","i":[{"t":"Text","s":"emphd"}]}]`,
			encoderHTML:   `<em class="zs-deprecated">emphd</em>`,
			encoderNative: `EmphD [Text "emphd"]`,
			encoderText:   "emphd",
			encoderZmk:    "__emphd__",
		},
	},
	{
		descr: "Strong formatting",
		zmk:   "**strong**",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Strong","i":[{"t":"Text","s":"strong"}]}]`,
			encoderHTML:   "<strong>strong</strong>",
			encoderNative: `Strong [Text "strong"]`,
			encoderText:   "strong",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Insert formatting",
		zmk:   ">>insert>>",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Insert","i":[{"t":"Text","s":"insert"}]}]`,
			encoderHTML:   "<ins>insert</ins>",
			encoderNative: `Insert [Text "insert"]`,
			encoderText:   "insert",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Delete formatting",
		zmk:   "~~delete~~",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Delete","i":[{"t":"Text","s":"delete"}]}]`,
			encoderHTML:   "<del>delete</del>",
			encoderNative: `Delete [Text "delete"]`,
			encoderText:   "delete",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Update formatting",
		zmk:   "~~old~~>>new>>",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Delete","i":[{"t":"Text","s":"old"}]},{"t":"Insert","i":[{"t":"Text","s":"new"}]}]`,
			encoderHTML:   "<del>old</del><ins>new</ins>",
			encoderNative: `Delete [Text "old"],Insert [Text "new"]`,
			encoderText:   "oldnew",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Monospace formatting",
		zmk:   "''monospace''",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Mono","i":[{"t":"Text","s":"monospace"}]}]`,
			encoderHTML:   `<span class="zs-monospace">monospace</span>`,
			encoderNative: `Mono [Text "monospace"]`,
			encoderText:   "monospace",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Superscript formatting",
		zmk:   "^^superscript^^",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Super","i":[{"t":"Text","s":"superscript"}]}]`,
			encoderHTML:   `<sup>superscript</sup>`,
			encoderNative: `Super [Text "superscript"]`,
			encoderText:   `superscript`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Subscript formatting",
		zmk:   ",,subscript,,",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Sub","i":[{"t":"Text","s":"subscript"}]}]`,
			encoderHTML:   `<sub>subscript</sub>`,
			encoderNative: `Sub [Text "subscript"]`,
			encoderText:   `subscript`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Quote","i":[{"t":"Text","s":"quotes"}]}]`,
			encoderHTML:   `"quotes"`,
			encoderNative: `Quote [Text "quotes"]`,
			encoderText:   `quotes`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Quote","a":{"lang":"de"},"i":[{"t":"Text","s":"quotes"}]}]`,
			encoderHTML:   `<span lang="de">&bdquo;quotes&ldquo;</span>`,
			encoderNative: `Quote ("",[lang="de"]) [Text "quotes"]`,
			encoderText:   `quotes`,
			encoderZmk:    `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Quotation formatting",
		zmk:   `<<quotation<<`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Quotation","i":[{"t":"Text","s":"quotation"}]}]`,
			encoderHTML:   `<q>quotation</q>`,
			encoderNative: `Quotation [Text "quotation"]`,
			encoderText:   `quotation`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Small formatting",
		zmk:   `;;small;;`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Small","i":[{"t":"Text","s":"small"}]}]`,
			encoderHTML:   `<small>small</small>`,
			encoderNative: `Small [Text "small"]`,
			encoderText:   `small`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Span formatting",
		zmk:   `::span::`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Span","i":[{"t":"Text","s":"span"}]}]`,
			encoderHTML:   `<span>span</span>`,
			encoderNative: `Span [Text "span"]`,
			encoderText:   `span`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Code formatting",
		zmk:   "``code``",
		expect: expectMap{
			encoderDJSON:  `[{"t":"Code","s":"code"}]`,
			encoderHTML:   `<code>code</code>`,
			encoderNative: `Code "code"`,
			encoderText:   `code`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Input formatting",
		zmk:   `++input++`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Input","s":"input"}]`,
			encoderHTML:   `<kbd>input</kbd>`,
			encoderNative: `Input "input"`,
			encoderText:   `input`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Output formatting",
		zmk:   `==output==`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Output","s":"output"}]`,
			encoderHTML:   `<samp>output</samp>`,
			encoderNative: `Output "output"`,
			encoderText:   `output`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]`,
			encoderHTML:   `<span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span>`,
			encoderNative: `Span ("",[lang="fr"]) [Quote [Text "abc"]]`,
			encoderText:   `abc`,
			encoderZmk:    `::""abc""::{lang="fr"}`,
		},
	},
	{
		descr: "Simple Citation",
		zmk:   `[@Stern18]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Cite","s":"Stern18"}]`,
			encoderHTML:   `Stern18`, // TODO
			encoderNative: `Cite "Stern18"`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "No comment",
		zmk:   `% comment`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Text","s":"%"},{"t":"Space"},{"t":"Text","s":"comment"}]`,
			encoderHTML:   `% comment`,
			encoderNative: `Text "%",Space,Text "comment"`,
			encoderText:   `% comment`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Line comment",
		zmk:   `%% line comment`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Comment","s":"line comment"}]`,
			encoderHTML:   `<!-- line comment -->`,
			encoderNative: `Comment "line comment"`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Comment after text",
		zmk:   `Text %% comment`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Text","s":"Text"},{"t":"Comment","s":"comment"}]`,
			encoderHTML:   `Text <!-- comment -->`,
			encoderNative: `Text "Text",Comment "comment"`,
			encoderText:   `Text`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple footnote",
		zmk:   `[^footnote]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Footnote","i":[{"t":"Text","s":"footnote"}]}]`,
			encoderHTML:   `<sup id="fnref:0"><a href="#fn:0" class="zs-footnote-ref" role="doc-noteref">0</a></sup>`,
			encoderNative: `Footnote [Text "footnote"]`,
			encoderText:   `footnote`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple mark",
		zmk:   `[!mark]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Mark","s":"mark"}]`,
			encoderHTML:   ``,
			encoderNative: `Mark "mark"`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Dummy Link",
		zmk:   `[[abc]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"external","s":"abc","i":[{"t":"Text","s":"abc"}]}]`,
			encoderHTML:   `<a href="abc" class="zs-external">abc</a>`,
			encoderNative: `Link EXTERNAL "abc" []`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple URL",
		zmk:   `[[https://zettelstore.de]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]}]`,
			encoderHTML:   `<a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a>`,
			encoderNative: `Link EXTERNAL "https://zettelstore.de" []`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "URL with Text",
		zmk:   `[[Home|https://zettelstore.de]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"Home"}]}]`,
			encoderHTML:   `<a href="https://zettelstore.de" class="zs-external">Home</a>`,
			encoderNative: `Link EXTERNAL "https://zettelstore.de" [Text "Home"]`,
			encoderText:   `Home`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]}]`,
			encoderHTML:   `<a href="00000000000100">00000000000100</a>`,
			encoderNative: `Link ZETTEL "00000000000100" []`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Zettel ID with Text",
		zmk:   `[[Config|00000000000100]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]}]`,
			encoderHTML:   `<a href="00000000000100">Config</a>`,
			encoderNative: `Link ZETTEL "00000000000100" [Text "Config"]`,
			encoderText:   `Config`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple Zettel ID with fragment",
		zmk:   `[[00000000000100#frag]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"zettel","s":"00000000000100#frag","i":[{"t":"Text","s":"00000000000100#frag"}]}]`,
			encoderHTML:   `<a href="00000000000100#frag">00000000000100#frag</a>`,
			encoderNative: `Link ZETTEL "00000000000100#frag" []`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Zettel ID with Text and fragment",
		zmk:   `[[Config|00000000000100#frag]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"zettel","s":"00000000000100#frag","i":[{"t":"Text","s":"Config"}]}]`,
			encoderHTML:   `<a href="00000000000100#frag">Config</a>`,
			encoderNative: `Link ZETTEL "00000000000100#frag" [Text "Config"]`,
			encoderText:   `Config`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Fragment link to self",
		zmk:   `[[#frag]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]}]`,
			encoderHTML:   `<a href="#frag">#frag</a>`,
			encoderNative: `Link SELF "#frag" []`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Hosted link",
		zmk:   `[[H|/hosted]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]}]`,
			encoderHTML:   `<a href="/hosted">H</a>`,
			encoderNative: `Link LOCAL "/hosted" [Text "H"]`,
			encoderText:   `H`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Based link",
		zmk:   `[[B|/based]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"local","s":"/based","i":[{"t":"Text","s":"B"}]}]`,
			encoderHTML:   `<a href="/based">B</a>`,
			encoderNative: `Link LOCAL "/based" [Text "B"]`,
			encoderText:   `B`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Relative link",
		zmk:   `[[R|../relative]]`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Link","q":"local","s":"../relative","i":[{"t":"Text","s":"R"}]}]`,
			encoderHTML:   `<a href="../relative">R</a>`,
			encoderNative: `Link LOCAL "../relative" [Text "R"]`,
			encoderText:   `R`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Dummy Embed",
		zmk:   `{{abc}}`,
		expect: expectMap{
			encoderDJSON:  `[{"t":"Embed","s":"abc"}]`,
			encoderHTML:   `<img src="abc" alt="">`,
			encoderNative: `Embed EXTERNAL "abc"`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderDJSON:  `[]`,
			encoderHTML:   ``,
			encoderNative: ``,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
}

Added encoder/encoder_test.go.































































































































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

package encoder_test

import (
	"bytes"
	"fmt"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"

	_ "zettelstore.de/z/encoder/djsonenc"  // Allow to use DJSON encoder.
	_ "zettelstore.de/z/encoder/htmlenc"   // Allow to use HTML encoder.
	_ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder.
	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

type zmkTestCase struct {
	descr  string
	zmk    string
	inline bool
	expect expectMap
}

type expectMap map[api.EncodingEnum]string

const useZmk = "\000"

const (
	encoderDJSON  = api.EncoderDJSON
	encoderHTML   = api.EncoderHTML
	encoderNative = api.EncoderNative
	encoderText   = api.EncoderText
	encoderZmk    = api.EncoderZmk
)

func TestEncoder(t *testing.T) {
	for i := range tcsInline {
		tcsInline[i].inline = true
	}
	executeTestCases(t, append(tcsBlock, tcsInline...))
}

func executeTestCases(t *testing.T, testCases []zmkTestCase) {
	t.Helper()
	for testNum, tc := range testCases {
		inp := input.NewInput([]byte(tc.zmk))
		var pe parserEncoder
		if tc.inline {
			pe = &peInlines{iln: parser.ParseInlines(inp, api.ValueSyntaxZmk)}
		} else {
			pe = &peBlocks{bln: parser.ParseBlocks(inp, nil, api.ValueSyntaxZmk)}
		}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk)
	}
}

func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) {
	t.Helper()
	for enc, exp := range expected {
		encdr := encoder.Create(enc, nil)
		got, err := pe.encode(encdr)
		if err != nil {
			t.Error(err)
			continue
		}
		if enc == api.EncoderZmk && exp == "\000" {
			exp = zmkDefault
		}
		if got != exp {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + pe.mode()
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}

type parserEncoder interface {
	encode(encoder.Encoder) (string, error)
	mode() string
}

type peInlines struct {
	iln *ast.InlineListNode
}

func (in peInlines) encode(encdr encoder.Encoder) (string, error) {
	var buf bytes.Buffer
	if _, err := encdr.WriteInlines(&buf, in.iln); err != nil {
		return "", err
	}
	return buf.String(), nil
}

func (peInlines) mode() string { return "inline" }

type peBlocks struct {
	bln *ast.BlockListNode
}

func (bl peBlocks) encode(encdr encoder.Encoder) (string, error) {
	var buf bytes.Buffer
	if _, err := encdr.WriteBlocks(&buf, bl.bln); err != nil {
		return "", err
	}
	return buf.String(), nil

}
func (peBlocks) mode() string { return "block" }

Changes to encoder/htmlenc/block.go.

29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
44
45
46

47
48
49
50
51
52
53
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53







-
+









-
+







		v.b.WriteString("<pre><code")
		v.visitAttributes(vn.Attrs)
		v.b.WriteByte('>')
		for _, line := range vn.Lines {
			v.writeHTMLEscaped(line)
			v.b.WriteByte('\n')
		}
		v.b.WriteString("</code></pre>\n")
		v.b.WriteString("</code></pre>")
		v.visibleSpace = oldVisible

	case ast.VerbatimComment:
		if vn.Attrs.HasDefault() {
			v.b.WriteString("<!--\n")
			for _, line := range vn.Lines {
				v.writeHTMLEscaped(line)
				v.b.WriteByte('\n')
			}
			v.b.WriteString("-->\n")
			v.b.WriteString("-->")
		}

	case ast.VerbatimHTML:
		for _, line := range vn.Lines {
			if !ignoreHTMLText(line) {
				v.b.WriteStrings(line, "\n")
			}
116
117
118
119
120
121
122
123

124
125

126
127

128
129
130
131
132
133
134
135

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

150
151
152
153
154
155
156
116
117
118
119
120
121
122

123
124

125
126

127
128
129
130
131
132
133
134

135
136
137
138
139
140
141
142
143
144
145
146
147
148

149
150
151
152
153
154
155
156







-
+

-
+

-
+







-
+













-
+







	defer v.lang.pop()

	v.b.WriteStrings("<", code)
	v.visitAttributes(attrs)
	v.b.WriteString(">\n")
	ast.Walk(v, rn.Blocks)
	if rn.Inlines != nil {
		v.b.WriteString("<cite>")
		v.b.WriteString("\n<cite>")
		ast.Walk(v, rn.Inlines)
		v.b.WriteString("</cite>\n")
		v.b.WriteString("</cite>")
	}
	v.b.WriteStrings("</", code, ">\n")
	v.b.WriteStrings("\n</", code, ">")
	v.inVerse = oldVerse
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	v.lang.push(hn.Attrs)
	defer v.lang.pop()

	lvl := hn.Level
	lvl := hn.Level + 1
	if lvl > 6 {
		lvl = 6 // HTML has H1..H6
	}
	strLvl := strconv.Itoa(lvl)
	v.b.WriteStrings("<h", strLvl)
	v.visitAttributes(hn.Attrs)
	if _, ok := hn.Attrs.Get("id"); !ok {
		if fragment := hn.Fragment; fragment != "" {
			v.b.WriteStrings(" id=\"", fragment, "\"")
		}
	}
	v.b.WriteByte('>')
	ast.Walk(v, hn.Inlines)
	v.b.WriteStrings("</h", strLvl, ">\n")
	v.b.WriteStrings("</h", strLvl, ">")
}

var mapNestedListKind = map[ast.NestedListKind]string{
	ast.NestedListOrdered:   "ol",
	ast.NestedListUnordered: "ul",
}

174
175
176
177
178
179
180
181

182
183
184
185
186
187
188
174
175
176
177
178
179
180

181
182
183
184
185
186
187
188







-
+







	v.visitAttributes(ln.Attrs)
	v.b.WriteString(">\n")
	for _, item := range ln.Items {
		v.b.WriteString("<li>")
		v.writeItemSliceOrPara(item, compact)
		v.b.WriteString("</li>\n")
	}
	v.b.WriteStrings("</", code, ">\n")
	v.b.WriteStrings("</", code, ">")
}

func (v *visitor) writeQuotationList(ln *ast.NestedListNode) {
	v.b.WriteString("<blockquote>\n")
	inPara := false
	for _, item := range ln.Items {
		if pn := getParaItem(item); pn != nil {
247
248
249
250
251
252
253




254



255
256
257
258
259
260
261
247
248
249
250
251
252
253
254
255
256
257

258
259
260
261
262
263
264
265
266
267







+
+
+
+
-
+
+
+







func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) {
	if compact && len(ins) == 1 {
		if para, ok := ins[0].(*ast.ParaNode); ok {
			ast.Walk(v, para.Inlines)
			return
		}
	}
	for i, in := range ins {
		if i >= 0 {
			v.b.WriteByte('\n')
		}
	ast.WalkItemSlice(v, ins)
		ast.Walk(v, in)
	}
	v.b.WriteByte('\n')
}

func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) {
	if len(ds) == 1 {
		if para, ok := ds[0].(*ast.ParaNode); ok {
			ast.Walk(v, para.Inlines)
			return
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
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







-
+
















-
+




-
-
-
+
+
+








		for _, b := range descr.Descriptions {
			v.b.WriteString("<dd>")
			v.writeDescriptionsSlice(b)
			v.b.WriteString("</dd>\n")
		}
	}
	v.b.WriteString("</dl>\n")
	v.b.WriteString("</dl>")
}

func (v *visitor) visitTable(tn *ast.TableNode) {
	v.b.WriteString("<table>\n")
	if len(tn.Header) > 0 {
		v.b.WriteString("<thead>\n")
		v.writeRow(tn.Header, "<th", "</th>")
		v.b.WriteString("</thead>\n")
	}
	if len(tn.Rows) > 0 {
		v.b.WriteString("<tbody>\n")
		for _, row := range tn.Rows {
			v.writeRow(row, "<td", "</td>")
		}
		v.b.WriteString("</tbody>\n")
	}
	v.b.WriteString("</table>\n")
	v.b.WriteString("</table>")
}

var alignStyle = map[ast.Alignment]string{
	ast.AlignDefault: ">",
	ast.AlignLeft:    " style=\"text-align:left\">",
	ast.AlignCenter:  " style=\"text-align:center\">",
	ast.AlignRight:   " style=\"text-align:right\">",
	ast.AlignLeft:    " class=\"zs-ta-left\">",
	ast.AlignCenter:  " class=\"zs-ta-center\">",
	ast.AlignRight:   " class=\"zs-ta-right\">",
}

func (v *visitor) writeRow(row ast.TableRow, cellStart, cellEnd string) {
	v.b.WriteString("<tr>")
	for _, cell := range row {
		v.b.WriteString(cellStart)
		if cell.Inlines.IsEmpty() {
322
323
324
325
326
327
328
329

330
331

332
333
334
335
336

337
328
329
330
331
332
333
334

335
336

337
338
339
340
341

342
343







-
+

-
+




-
+

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")
		v.b.WriteString("\">")
	default:
		v.b.WriteStrings("<p class=\"error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>\n")
		v.b.WriteStrings("<p class=\"zs-error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>")
	}
}

func (v *visitor) writeEndPara() {
	v.b.WriteString("</p>\n")
	v.b.WriteString("</p>")
}

Changes to encoder/htmlenc/htmlenc.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
24







-
+








// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"io"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderHTML, encoder.Info{
38
39
40
41
42
43
44
45

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

45
46
47
48
49
50
51
52







-
+







	}
	if env := he.env; env != nil && env.Lang == "" {
		v.b.WriteStrings("<html>\n<head>")
	} else {
		v.b.WriteStrings("<html lang=\"", env.Lang, "\">")
	}
	v.b.WriteString("\n<head>\n<meta charset=\"utf-8\">\n")
	plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle)
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		v.b.WriteStrings("<title>", v.evalValue(plainTitle, evalMeta), "</title>")
	}
	v.acceptMeta(zn.InhMeta, evalMeta)
	v.b.WriteString("\n</head>\n<body>\n")
	if hasTitle {
		if ilnTitle := evalMeta(plainTitle); ilnTitle != nil {
63
64
65
66
67
68
69
70
71


72
73
74
75
76
77
78
63
64
65
66
67
68
69


70
71
72
73
74
75
76
77
78







-
-
+
+







}

// WriteMeta encodes meta data as HTML5.
func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(he, w)

	// Write title
	if title, ok := m.Get(meta.KeyTitle); ok {
		v.b.WriteStrings("<meta name=\"zs-", meta.KeyTitle, "\" content=\"")
	if title, ok := m.Get(api.KeyTitle); ok {
		v.b.WriteStrings("<meta name=\"zs-", api.KeyTitle, "\" content=\"")
		v.writeQuotedEscaped(v.evalValue(title, evalMeta))
		v.b.WriteString("\">")
	}

	// Write other metadata
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()

Changes to encoder/htmlenc/inline.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







-
-
+
+







package htmlenc

import (
	"fmt"
	"strconv"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
)

func (v *visitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		if v.env.IsXHTML() {
			v.b.WriteString("<br />\n")
		} else {
155
156
157
158
159
160
161
162
163


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190

191
192
193
194
195
196
197
198
155
156
157
158
159
160
161


162
163
164
165


166
167


168
169


170
171
172
173
174
175
176
177
178
179
180
181
182
183

184

185
186
187
188
189
190
191







-
-
+
+


-
-


-
-


-
-














-
+
-







func (v *visitor) visitFormat(fn *ast.FormatNode) {
	v.lang.push(fn.Attrs)
	defer v.lang.pop()

	var code string
	attrs := fn.Attrs.Clone()
	switch fn.Kind {
	case ast.FormatItalic:
		code = "i"
	case ast.FormatEmphDeprecated:
		code, attrs = "em", attrs.AddClass("zs-deprecated")
	case ast.FormatEmph:
		code = "em"
	case ast.FormatBold:
		code = "b"
	case ast.FormatStrong:
		code = "strong"
	case ast.FormatUnder:
		code = "u" // TODO: ändern in <span class="XXX">
	case ast.FormatInsert:
		code = "ins"
	case ast.FormatStrike:
		code = "s"
	case ast.FormatDelete:
		code = "del"
	case ast.FormatSuper:
		code = "sup"
	case ast.FormatSub:
		code = "sub"
	case ast.FormatQuotation:
		code = "q"
	case ast.FormatSmall:
		code = "small"
	case ast.FormatSpan:
		v.writeSpan(fn.Inlines, processSpanAttributes(attrs))
		return
	case ast.FormatMonospace:
		code = "span"
		code, attrs = "span", attrs.AddClass("zs-monospace")
		attrs = attrs.Set("style", "font-family:monospace")
	case ast.FormatQuote:
		v.visitQuotes(fn)
		return
	default:
		panic(fmt.Sprintf("Unknown format kind %v", fn.Kind))
	}
	v.b.WriteStrings("<", code)
208
209
210
211
212
213
214
215
216
217



218
219
220
221
222
223
224
201
202
203
204
205
206
207



208
209
210
211
212
213
214
215
216
217







-
-
-
+
+
+







	v.b.WriteByte('>')
	ast.Walk(v, iln)
	v.b.WriteString("</span>")

}

var langQuotes = map[string][2]string{
	meta.ValueLangEN: {"&ldquo;", "&rdquo;"},
	"de":             {"&bdquo;", "&ldquo;"},
	"fr":             {"&laquo;&nbsp;", "&nbsp;&raquo;"},
	api.ValueLangEN: {"&ldquo;", "&rdquo;"},
	"de":            {"&bdquo;", "&ldquo;"},
	"fr":            {"&laquo;&nbsp;", "&nbsp;&raquo;"},
}

func getQuotes(lang string) (string, string) {
	langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' })
	for len(langFields) > 0 {
		langSup := strings.Join(langFields, "-")
		quotes, ok := langQuotes[langSup]
251
252
253
254
255
256
257



258
259
260
261
262
263
264
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260







+
+
+







	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:
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
		}
		v.b.WriteString("<!-- ")
		v.writeHTMLEscaped(ln.Text) // writeCommentEscaped
		v.b.WriteString(" -->")
	case ast.LiteralHTML:
		if !ignoreHTMLText(ln.Text) {
			v.b.WriteString(ln.Text)
		}

Changes to encoder/htmlenc/visitor.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
8
9
10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

82
83

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

97
98
99
100
101
102
103







+





-
+















+

















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














-
+

-
+












-







// under this license.
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"bytes"
	"io"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/strfun"
)

// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
	env           *encoder.Environment
	b             encoder.BufWriter
	visibleSpace  bool // Show space character in plain text
	inVerse       bool // In verse block
	inInteractive bool // Rendered interactive HTML code
	lang          langStack
	textEnc       encoder.Encoder
	inlinePos     int // Element position in inline list node
}

func newVisitor(he *htmlEncoder, w io.Writer) *visitor {
	var lang string
	if he.env != nil {
		lang = he.env.Lang
	}
	return &visitor{
		env:     he.env,
		b:       encoder.NewBufWriter(w),
		lang:    newLangStack(lang),
		textEnc: encoder.Create(api.EncoderText, nil),
	}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockListNode:
		for i, bn := range n.List {
			if i > 0 {
				v.b.WriteByte('\n')
			}
			ast.Walk(v, bn)
		}
	case *ast.InlineListNode:
		for i, in := range n.List {
			v.inlinePos = i
			ast.Walk(v, in)
		}
		v.inlinePos = 0
	case *ast.ParaNode:
		v.b.WriteString("<p>")
		ast.Walk(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")
			v.b.WriteString(" />")
		} else {
			v.b.WriteString(">\n")
			v.b.WriteBytes('>')
		}
	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 {
108
109
110
111
112
113
114
115
116


117
118
119
120
121
122


123
124
125
126



127
128

129
130
131
132
133
134
135
122
123
124
125
126
127
128


129
130
131
132
133
134


135
136
137



138
139
140
141

142
143
144
145
146
147
148
149







-
-
+
+




-
-
+
+

-
-
-
+
+
+

-
+







	default:
		return v
	}
	return nil
}

var mapMetaKey = map[string]string{
	meta.KeyCopyright: "copyright",
	meta.KeyLicense:   "license",
	api.KeyCopyright: "copyright",
	api.KeyLicense:   "license",
}

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	ignore := v.setupIgnoreSet()
	ignore[meta.KeyTitle] = true
	if tags, ok := m.Get(meta.KeyAllTags); ok {
	ignore[api.KeyTitle] = true
	if tags, ok := m.Get(api.KeyAllTags); ok {
		v.writeTags(tags)
		ignore[meta.KeyAllTags] = true
		ignore[meta.KeyTags] = true
	} else if tags, ok := m.Get(meta.KeyTags); ok {
		ignore[api.KeyAllTags] = true
		ignore[api.KeyTags] = true
	} else if tags, ok = m.Get(api.KeyTags); ok {
		v.writeTags(tags)
		ignore[meta.KeyTags] = true
		ignore[api.KeyTags] = true
	}

	for _, p := range m.Pairs(true) {
		key := p.Key
		if ignore[key] {
			continue
		}
144
145
146
147
148
149
150
151
152


153
154

155
156
157
158
159
160
161
158
159
160
161
162
163
164


165
166
167

168
169
170
171
172
173
174
175







-
-
+
+

-
+







		} else {
			v.writeMeta("zs-", key, value)
		}
	}
}

func (v *visitor) evalValue(value string, evalMeta encoder.EvalMetaFunc) string {
	var sb strings.Builder
	_, err := v.textEnc.WriteInlines(&sb, evalMeta(value))
	var buf bytes.Buffer
	_, err := v.textEnc.WriteInlines(&buf, evalMeta(value))
	if err == nil {
		return sb.String()
		return buf.String()
	}
	return ""
}

func (v *visitor) setupIgnoreSet() map[string]bool {
	if v.env == nil || v.env.IgnoreMeta == nil {
		return make(map[string]bool)

Changes to encoder/nativeenc/nativeenc.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 (
	"fmt"
	"io"
	"sort"
	"strconv"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderNative, encoder.Info{
184
185
186
187
188
189
190
191
192
193
194




195
196
197
198
199
200
201
184
185
186
187
188
189
190




191
192
193
194
195
196
197
198
199
200
201







-
-
-
-
+
+
+
+







var (
	rawBackslash   = []byte{'\\', '\\'}
	rawDoubleQuote = []byte{'\\', '"'}
	rawNewline     = []byte{'\\', 'n'}
)

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	v.writeZettelmarkup("Title", m.GetDefault(meta.KeyTitle, ""), evalMeta)
	v.writeMetaString(m, meta.KeyRole, "Role")
	v.writeMetaList(m, meta.KeyTags, "Tags")
	v.writeMetaString(m, meta.KeySyntax, "Syntax")
	v.writeZettelmarkup("Title", m.GetDefault(api.KeyTitle, ""), evalMeta)
	v.writeMetaString(m, api.KeyRole, "Role")
	v.writeMetaList(m, api.KeyTags, "Tags")
	v.writeMetaString(m, api.KeySyntax, "Syntax")
	pairs := m.PairsRest(true)
	if len(pairs) == 0 {
		return
	}
	v.b.WriteString("\n[Header")
	v.level++
	for i, p := range pairs {
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
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







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







	if fragment := mn.Fragment; fragment != "" {
		v.b.WriteString(" #")
		v.writeEscaped(fragment)
	}
}

var mapFormatKind = map[ast.FormatKind][]byte{
	ast.FormatItalic:    []byte("Italic"),
	ast.FormatEmph:      []byte("Emph"),
	ast.FormatEmphDeprecated: []byte("EmphD"),
	ast.FormatEmph:           []byte("Emph"),
	ast.FormatBold:      []byte("Bold"),
	ast.FormatStrong:    []byte("Strong"),
	ast.FormatStrong:         []byte("Strong"),
	ast.FormatUnder:     []byte("Underline"),
	ast.FormatInsert:    []byte("Insert"),
	ast.FormatMonospace: []byte("Mono"),
	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"),
	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"),
}

var mapLiteralKind = map[ast.LiteralKind][]byte{
	ast.LiteralProg:    []byte("Code"),
	ast.LiteralKeyb:    []byte("Input"),
	ast.LiteralOutput:  []byte("Output"),
	ast.LiteralComment: []byte("Comment"),

Changes to encoder/textenc/textenc.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
24







-
+








// Package textenc encodes the abstract syntax tree into its text.
package textenc

import (
	"io"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderText, encoder.Info{
93
94
95
96
97
98
99
100


101
102
103
104
105
106
107
108
109
110







111
112
113
114
115
116
117
93
94
95
96
97
98
99

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125







-
+
+










+
+
+
+
+
+
+







	ast.Walk(v, iln)
	length, err := v.b.Flush()
	return length, err
}

// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
	b encoder.BufWriter
	b         encoder.BufWriter
	inlinePos int
}

func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewBufWriter(w)}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockListNode:
		v.visitBlockList(n)
	case *ast.InlineListNode:
		for i, in := range n.List {
			v.inlinePos = i
			ast.Walk(v, in)
		}
		v.inlinePos = 0
		return nil
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
		return nil
	case *ast.RegionNode:
		v.visitBlockList(n.Blocks)
		if n.Inlines != nil {
			v.b.WriteByte('\n')
145
146
147
148
149
150
151

152
153



154
155
156
157
158
159
160
153
154
155
156
157
158
159
160


161
162
163
164
165
166
167
168
169
170







+
-
-
+
+
+







		return nil
	case *ast.LinkNode:
		if !n.OnlyRef {
			ast.Walk(v, n.Inlines)
		}
		return nil
	case *ast.FootnoteNode:
		if v.inlinePos > 0 {
		v.b.WriteByte(' ')
		return v // No 'return nil' to write text
			v.b.WriteByte(' ')
		}
		// No 'return nil' to write text
	case *ast.LiteralNode:
		if n.Kind != ast.LiteralComment {
			v.b.WriteString(n.Text)
		}
	}
	return v
}

Changes to encoder/zmkenc/zmkenc.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
26







-
+







package zmkenc

import (
	"fmt"
	"io"
	"sort"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderZmk, encoder.Info{
83
84
85
86
87
88
89
90
91
92




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











109

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

130
131
132
133
134
135
136
83
84
85
86
87
88
89



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104





105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135

136
137
138
139
140
141
142
143







-
-
-
+
+
+
+











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

+









-









-
+







	ast.Walk(v, iln)
	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
	b         encoder.BufWriter
	prefix    []byte
	enc       *zmkEncoder
	inlinePos int
}

func newVisitor(w io.Writer, enc *zmkEncoder) *visitor {
	return &visitor{
		b:   encoder.NewBufWriter(w),
		enc: enc,
	}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.ParaNode:
		ast.Walk(v, n.Inlines)
		v.b.WriteByte('\n')
		if len(v.prefix) == 0 {
			v.b.WriteByte('\n')
	case *ast.BlockListNode:
		for i, bn := range n.List {
			if i > 0 {
				v.b.WriteByte('\n')
			}
			ast.Walk(v, bn)
		}
	case *ast.InlineListNode:
		for i, in := range n.List {
			v.inlinePos = i
			ast.Walk(v, in)
		}
		v.inlinePos = 0
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.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")
			"' and syntax '", n.Syntax, "'.")
	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:
153
154
155
156
157
158
159
160






161





162
163

164
165
166
167
168
169

170
171
172
173
174
175
176
177
178
179
180
181
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180

181
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








+
+
+
+
+
+

+
+
+
+
+

-
+





-
+


















+





-



-
-
+
+
-
-


-










-
+
+
+
+


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






+
+
+
+
-
+
+



-
+
+
+
+


-


-
+

-












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







	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimComment: "%%%",
	ast.VerbatimHTML:    "???",
	ast.VerbatimProg:    "```",
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind))
	}

	// TODO: scan cn.Lines to find embedded "`"s at beginning
	v.b.WriteString("```")
	v.b.WriteString(kind)
	v.visitAttributes(vn.Attrs)
	v.b.WriteByte('\n')
	for _, line := range vn.Lines {
		v.b.WriteStrings(line, "\n")
	}
	v.b.WriteString("```\n")
	v.b.WriteString(kind)
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  ":::",
	ast.RegionQuote: "<<<",
	ast.RegionVerse: "\"\"\"",
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	// Scan rn.Blocks for embedded regions to adjust length of regionCode
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %d", rn.Kind))
	}
	v.b.WriteString(kind)
	v.visitAttributes(rn.Attrs)
	v.b.WriteByte('\n')
	ast.Walk(v, rn.Blocks)
	v.b.WriteByte('\n')
	v.b.WriteString(kind)
	if rn.Inlines != nil {
		v.b.WriteByte(' ')
		ast.Walk(v, rn.Inlines)
	}
	v.b.WriteByte('\n')
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	for i := 0; i <= hn.Level; i++ {
		v.b.WriteByte('=')
	const headingSigns = "========= "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
	}
	v.b.WriteByte(' ')
	ast.Walk(v, hn.Inlines)
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte('\n')
}

var mapNestedListKind = map[ast.NestedListKind]byte{
	ast.NestedListOrdered:   '#',
	ast.NestedListUnordered: '*',
	ast.NestedListQuote:     '>',
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	v.prefix = append(v.prefix, mapNestedListKind[ln.Kind])
	for _, item := range ln.Items {
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.b.Write(v.prefix)
		v.b.WriteByte(' ')
		for i, in := range item {
			if i > 0 {
		for j, in := range item {
			if j > 0 {
				if _, ok := in.(*ast.ParaNode); ok {
					v.b.WriteByte('\n')
					for j := 0; j <= len(v.prefix); j++ {
						v.b.WriteByte(' ')
				v.b.WriteByte('\n')
				if _, ok := in.(*ast.ParaNode); ok {
					v.writePrefixSpaces()
					}
				}
			}
			ast.Walk(v, in)
		}
	}
	v.prefix = v.prefix[:len(v.prefix)-1]
}

func (v *visitor) writePrefixSpaces() {
	for i := 0; i <= len(v.prefix); i++ {
	v.b.WriteByte('\n')
		v.b.WriteByte(' ')
	}
}

func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	for _, descr := range dn.Descriptions {
	for i, descr := range dn.Descriptions {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.b.WriteString("; ")
		ast.Walk(v, descr.Term)
		v.b.WriteByte('\n')

		for _, b := range descr.Descriptions {
			v.b.WriteString(": ")
			v.b.WriteString("\n: ")
			ast.WalkDescriptionSlice(v, b)
			v.b.WriteByte('\n')
		}
	}
}

var alignCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
	ast.AlignCenter:  ":",
	ast.AlignRight:   ">",
}

func (v *visitor) visitTable(tn *ast.TableNode) {
	if header := tn.Header; len(header) > 0 {
		v.writeTableHeader(header, tn.Align)
		v.b.WriteByte('\n')
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.writeTableRow(row, tn.Align)
	}
}
	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.Walk(v, cell.Inlines)
			if colAlign != ast.AlignDefault {
				v.b.WriteString(alignCode[colAlign])
			}
		}

func (v *visitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
	for pos, cell := range header {
		v.b.WriteString("|=")
		colAlign := align[pos]
		if cell.Align != colAlign {
			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, cell.Inlines)
		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.Walk(v, cell.Inlines)
		}
}

func (v *visitor) writeTableRow(row ast.TableRow, align []ast.Alignment) {
	for pos, cell := range row {
		v.b.WriteByte('|')
		if cell.Align != align[pos] {
			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, cell.Inlines)
	}
		v.b.WriteByte('\n')
	}
	v.b.WriteByte('\n')
}

var escapeSeqs = map[string]bool{
	"\\":   true,
	"//":   true,
	"**":   true,
	"__":   true,
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
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







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







-
-
-
-
-
-
-



-
+











+
+
+







		ast.Walk(v, cn.Inlines)
	}
	v.b.WriteByte(']')
	v.visitAttributes(cn.Attrs)
}

var mapFormatKind = map[ast.FormatKind][]byte{
	ast.FormatItalic:    []byte("//"),
	ast.FormatEmph:      []byte("//"),
	ast.FormatEmphDeprecated: []byte("__"),
	ast.FormatEmph:           []byte("__"),
	ast.FormatBold:      []byte("**"),
	ast.FormatStrong:    []byte("**"),
	ast.FormatStrong:         []byte("**"),
	ast.FormatUnder:     []byte("__"),
	ast.FormatInsert:    []byte("__"),
	ast.FormatInsert:         []byte(">>"),
	ast.FormatStrike:    []byte("~~"),
	ast.FormatDelete:    []byte("~~"),
	ast.FormatSuper:     []byte("^^"),
	ast.FormatSub:       []byte(",,"),
	ast.FormatQuotation: []byte("<<"),
	ast.FormatQuote:     []byte("\"\""),
	ast.FormatSmall:     []byte(";;"),
	ast.FormatSpan:      []byte("::"),
	ast.FormatMonospace: []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("''"),
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))
	}
	attrs := fn.Attrs
	switch fn.Kind {
	case ast.FormatEmph, ast.FormatStrong, ast.FormatInsert, ast.FormatDelete:
		attrs = attrs.Clone()
		attrs.Set("-", "")
	}

	v.b.Write(kind)
	ast.Walk(v, fn.Inlines)
	v.b.Write(kind)
	v.visitAttributes(attrs)
	v.visitAttributes(fn.Attrs)
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	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:
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
		}
		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))

Changes to evaluator/evaluator.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
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32




33
34
35
36
37
38
39
40
41
42
43







+












-
-
-
-
+
+
+
+








import (
	"context"
	"errors"
	"fmt"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser/cleaner"
)

// Environment contains values to control the evaluation.
type Environment struct {
	EmbedImage   bool
	GetTagRef    func(string) *ast.Reference
	GetHostedRef func(string) *ast.Reference
	GetFoundRef  func(zid id.Zid, fragment string) *ast.Reference
	GetTagRef        func(string) *ast.Reference
	GetHostedRef     func(string) *ast.Reference
	GetFoundRef      func(zid id.Zid, fragment string) *ast.Reference
	GetImageMaterial func(zettel domain.Zettel, syntax string) ast.MaterialNode
}

// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
type Port interface {
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	GetZettel(context.Context, id.Zid) (domain.Zettel, error)
}
62
63
64
65
66
67
68
69

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

83
84
85
86





87
88
89
90
91
92
93
63
64
65
66
67
68
69

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

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







-
+












-
+




+
+
+
+
+







		env = &emptyEnv
	}
	e := evaluator{
		ctx:        ctx,
		port:       port,
		env:        env,
		rtConfig:   rtConfig,
		astMap:     map[id.Zid]*ast.ZettelNode{},
		costMap:    map[id.Zid]embedCost{},
		embedMap:   map[string]*ast.InlineListNode{},
		embedCount: 0,
		marker:     &ast.ZettelNode{},
	}
	ast.Walk(&e, n)
}

type evaluator struct {
	ctx        context.Context
	port       Port
	env        *Environment
	rtConfig   config.Config
	astMap     map[id.Zid]*ast.ZettelNode
	costMap    map[id.Zid]embedCost
	marker     *ast.ZettelNode
	embedMap   map[string]*ast.InlineListNode
	embedCount int
}

type embedCost struct {
	zn *ast.ZettelNode
	ec int
}

func (e *evaluator) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineListNode:
		e.visitInlineList(n)
	default:
		return e
101
102
103
104
105
106
107
108
109


110
111
112
113

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


114
115
116
117
118

119
120
121
122
123
124
125
126







-
-
+
+



-
+







		ast.Walk(e, in)
		switch n := in.(type) {
		case *ast.TagNode:
			iln.List[i] = e.visitTag(n)
		case *ast.LinkNode:
			iln.List[i] = e.evalLinkNode(n)
		case *ast.EmbedNode:
			in := e.evalEmbedNode(n)
			if ln, ok := in.(*ast.InlineListNode); ok {
			in2 := e.evalEmbedNode(n)
			if ln, ok := in2.(*ast.InlineListNode); ok {
				iln.List = replaceWithInlineNodes(iln.List, i, ln.List)
				i += len(ln.List) - 1
			} else {
				iln.List[i] = in
				iln.List[i] = in2
			}
		}
	}
}

func replaceWithInlineNodes(ins []ast.InlineNode, i int, replaceIns []ast.InlineNode) []ast.InlineNode {
	if len(replaceIns) == 1 {
178
179
180
181
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
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







+
+
+
+
+
+










-
+
+



+

-
-












+



-
+
-
-
+
-
-
+

+



-
+
+

+



+
-
-
-
+
+
+
+

+






+
-
-
+
+
+
+



+
-
+
-
-








-
+


+
+
+
+
+
+
+

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




-
-
-
-
+
+
+
+





-
-
+
-
-
-
-

-
+





-
+






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







	if gfr := e.env.GetFoundRef; gfr != nil {
		ln.Ref = gfr(zid, ref.URL.EscapedFragment())
	}
	return ln
}

func (e *evaluator) evalEmbedNode(en *ast.EmbedNode) ast.InlineNode {
	if maxTrans := e.rtConfig.GetMaxTransclusions(); e.embedCount > maxTrans {
		e.embedCount = maxTrans + 1 // To prevent e.embedCount from counting
		return createErrorText(en,
			"Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",",
			"see", "runtime", "configuration", "key", "max-transclusions):")
	}
	switch en.Material.(type) {
	case *ast.ReferenceMaterialNode:
	case *ast.BLOBMaterialNode:
		return en
	default:
		panic(fmt.Sprintf("Unknown material type %t for %v", en.Material, en.Material))
	}

	ref := en.Material.(*ast.ReferenceMaterialNode)
	switch ref.Ref.State {
	case ast.RefStateInvalid:
	case ast.RefStateInvalid, ast.RefStateBroken:
		e.embedCount++
		return e.createErrorImage(en)
	case ast.RefStateZettel, ast.RefStateFound:
	case ast.RefStateSelf:
		e.embedCount++
		return createErrorText(en, "Self", "embed", "reference:")
	case ast.RefStateBroken:
		return e.createErrorImage(en)
	case ast.RefStateHosted, ast.RefStateBased, ast.RefStateExternal:
		return en
	default:
		panic(fmt.Sprintf("Unknown state %v for reference %v", ref.Ref.State, ref.Ref))
	}

	zid, err := id.Parse(ref.Ref.URL.Path)
	if err != nil {
		panic(err)
	}
	zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if err != nil {
		e.embedCount++
		return e.createErrorImage(en)
	}

	syntax := e.getSyntax(zettel.Meta)
	if syntax := e.getSyntax(zettel.Meta); parser.IsImageFormat(syntax) {
	if parser.IsImageFormat(syntax) {
		return e.embedImage(en, zettel, syntax)
		return e.embedImage(en, zettel)
	}
	if !parser.IsTextParser(syntax) {
	} else if !parser.IsTextParser(syntax) {
		// Not embeddable.
		e.embedCount++
		return createErrorText(en, "Not", "embeddable (syntax="+syntax+"):")
	}

	zn, ok := e.astMap[zid]
	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.embedCount++
		return createErrorText(en, "Recursive", "transclusion:")
	}
	if !ok {
		ec := e.embedCount
		e.astMap[zid] = e.marker
		zn = e.evaluateEmbeddedZettel(zettel, syntax)
		e.astMap[zid] = zn
		e.costMap[zid] = embedCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		e.costMap[zid] = embedCost{zn: zn, ec: e.embedCount - ec}
		e.embedCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
	}
	e.embedCount++

	result, ok := e.embedMap[ref.Ref.Value]
	if !ok {
		// Search for text to be embedded.
		result = findInlineList(zn.Ast, ref.Ref.URL.Fragment)
		e.embedMap[ref.Ref.Value] = result
	}
		if result.IsEmpty() {
			return createErrorText(en, "Nothing", "to", "transclude:")
	if result.IsEmpty() {
		return &ast.LiteralNode{
			Kind: ast.LiteralComment,
			Text: "Nothing to transclude: " + en.Material.(*ast.ReferenceMaterialNode).Ref.String(),
		}
	}

	if ec := cost.ec; ec > 0 {
	e.embedCount++
		e.embedCount += cost.ec
	if maxTrans := e.rtConfig.GetMaxTransclusions(); e.embedCount > maxTrans {
		return createErrorText(en, "Too", "many", "transclusions ("+strconv.Itoa(maxTrans)+"):")
	}
	return result
}

func (e *evaluator) getSyntax(m *meta.Meta) string {
	if cfg := e.rtConfig; cfg != nil {
		return config.GetSyntax(m, cfg)
	}
	return m.GetDefault(meta.KeySyntax, "")
	return m.GetDefault(api.KeySyntax, "")
}

func (e *evaluator) getTitle(m *meta.Meta) string {
	if cfg := e.rtConfig; cfg != nil {
		return config.GetTitle(m, cfg)
	}
	return m.GetDefault(api.KeyTitle, "")
}

func (e *evaluator) createErrorImage(en *ast.EmbedNode) *ast.EmbedNode {
	zid := id.EmojiZid
	if !e.env.EmbedImage {
	errorZid := id.EmojiZid
	if gim := e.env.GetImageMaterial; gim != nil {
		en.Material = &ast.ReferenceMaterialNode{Ref: ast.ParseReference(zid.String())}
		return en
	}
	zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if err == nil {
		return doEmbedImage(en, zettel, e.getSyntax(zettel.Meta))
	}
		zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), errorZid)
		if err != nil {
			panic(err)
		}
		en.Material = gim(zettel, e.getSyntax(zettel.Meta))
		if en.Inlines == nil {
			if title := e.getTitle(zettel.Meta); title != "" {
				en.Inlines = parser.ParseMetadata(title)
			}
	panic(err)
}

		}
		return en
	}
func (e *evaluator) embedImage(en *ast.EmbedNode, zettel domain.Zettel, syntax string) *ast.EmbedNode {
	if e.env.EmbedImage {
	en.Material = &ast.ReferenceMaterialNode{Ref: ast.ParseReference(errorZid.String())}
	if en.Inlines == nil {
		return doEmbedImage(en, zettel, syntax)
		en.Inlines = parser.ParseMetadata("Error placeholder")
	}
	return en
}

func doEmbedImage(en *ast.EmbedNode, zettel domain.Zettel, syntax string) *ast.EmbedNode {
	en.Material = &ast.BLOBMaterialNode{
		Blob:   zettel.Content.AsBytes(),
		Syntax: syntax,
func (e *evaluator) embedImage(en *ast.EmbedNode, zettel domain.Zettel) *ast.EmbedNode {
	if gim := e.env.GetImageMaterial; gim != nil {
		en.Material = gim(zettel, e.getSyntax(zettel.Meta))
		return en
	}
	return en
}

func createErrorText(en *ast.EmbedNode, msgWords ...string) ast.InlineNode {
	ref := en.Material.(*ast.ReferenceMaterialNode)
	ln := &ast.LinkNode{
	ln := linkNodeToEmbeddedReference(en)
		Ref:     ref.Ref,
		Inlines: ast.CreateInlineListNodeFromWords(ref.Ref.String()),
		OnlyRef: true,
	}
	text := ast.CreateInlineListNodeFromWords(msgWords...)
	text.Append(&ast.SpaceNode{Lexeme: " "}, ln)
	text.Append(&ast.SpaceNode{Lexeme: " "}, ln, &ast.TextNode{Text: "."}, &ast.SpaceNode{Lexeme: " "})
	fn := &ast.FormatNode{
		Kind:    ast.FormatMonospace,
		Inlines: text,
	}
	fn = &ast.FormatNode{
		Kind:    ast.FormatBold,
		Kind:    ast.FormatStrong,
		Inlines: ast.CreateInlineListNode(fn),
	}
	fn.Attrs = fn.Attrs.AddClass("error")
	return fn
}

func linkNodeToEmbeddedReference(en *ast.EmbedNode) *ast.LinkNode {
	ref := en.Material.(*ast.ReferenceMaterialNode)
	ln := &ast.LinkNode{
		Ref:     ref.Ref,
		Inlines: ast.CreateInlineListNodeFromWords(ref.Ref.String()),
		OnlyRef: true,
	}
	return ln
}

func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel, syntax string) *ast.ZettelNode {
	zn := parser.ParseZettel(zettel, syntax, e.rtConfig)
func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel) *ast.ZettelNode {
	zn := parser.ParseZettel(zettel, e.getSyntax(zettel.Meta), e.rtConfig)
	ast.Walk(e, zn.Ast)
	return zn
}

func findInlineList(bnl *ast.BlockListNode, fragment string) *ast.InlineListNode {
	if fragment == "" {
		return firstFirstTopLevelParagraph(bnl.List)

Changes to go.mod.

1
2
3
4
5
6
7
8

9
10
11

12
13
14
1
2
3
4
5
6
7

8
9
10
11
12
13
14
15







-
+



+



module zettelstore.de/z

go 1.17

require (
	github.com/fsnotify/fsnotify v1.5.1
	github.com/pascaldekloe/jwt v1.10.0
	github.com/yuin/goldmark v1.4.1
	github.com/yuin/goldmark v1.4.3
	golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
	golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
	golang.org/x/text v0.3.7
	zettelstore.de/c v0.0.0-20211025140135-ccc3f543d0e9
)

require golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect

Changes to go.sum.

1
2
3
4
5
6


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


1
2
3
4


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




-
-
+
+














+
+
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
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.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.3 h1:eTEYYWtQLjK7+WK45Tk81OTkp/0UvAyqUj8flU0nTO4=
github.com/yuin/goldmark v1.4.3/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
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=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-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.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
zettelstore.de/c v0.0.0-20211025140135-ccc3f543d0e9 h1:X6Kb1JO0SEFxNrXiELXJeDxIMhjv0l0UaXUnRkAofjY=
zettelstore.de/c v0.0.0-20211025140135-ccc3f543d0e9/go.mod h1:Hx/qzHCaQ8zzXEzBglBj/2aGkQpBQG81/4XztCIGJ84=

Changes to input/input.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
46
47

48
49
50
51
52
53
54
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31

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

47
48
49
50
51
52
53
54







-
+








-
+














-
+







	"unicode"
	"unicode/utf8"
)

// Input is an abstract input source
type Input struct {
	// Read-only, will never change
	Src string // The source string
	Src []byte // The source string

	// Read-only, will change
	Ch      rune // current character
	Pos     int  // character position in src
	readPos int  // reading position (position after current character)
}

// NewInput creates a new input source.
func NewInput(src string) *Input {
func NewInput(src []byte) *Input {
	inp := &Input{Src: src}
	inp.Next()
	return inp
}

// EOS = End of source
const EOS = rune(-1)

// Next reads the next rune into inp.Ch.
func (inp *Input) Next() {
	if inp.readPos < len(inp.Src) {
		inp.Pos = inp.readPos
		r, w := rune(inp.Src[inp.readPos]), 1
		if r >= utf8.RuneSelf {
			r, w = utf8.DecodeRuneInString(inp.Src[inp.readPos:])
			r, w = utf8.DecodeRune(inp.Src[inp.readPos:])
		}
		inp.readPos += w
		inp.Ch = r
	} else {
		inp.Pos = len(inp.Src)
		inp.Ch = EOS
	}
63
64
65
66
67
68
69
70

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

70
71
72
73
74
75
76
77







-
+







// PeekN returns the n-th rune after the most recently read rune without
// advancing. If end-of-source was already found peek returns EOS.
func (inp *Input) PeekN(n int) rune {
	pos := inp.readPos + n
	if pos < len(inp.Src) {
		r := rune(inp.Src[pos])
		if r >= utf8.RuneSelf {
			r, _ = utf8.DecodeRuneInString(inp.Src[pos:])
			r, _ = utf8.DecodeRune(inp.Src[pos:])
		}
		if r == '\t' {
			return ' '
		}
		return r
	}
	return EOS
191
192
193
194
195
196
197
198

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

198
199
200
201
202
203
204
205
206
207







-
+









func (inp *Input) scanEntityNamed(pos int) (string, bool) {
	for {
		switch inp.Ch {
		case EOS, '\n', '\r':
			return "", false
		case ';':
			inp.Next()
			es := inp.Src[pos:inp.Pos]
			es := string(inp.Src[pos:inp.Pos])
			ues := html.UnescapeString(es)
			if es == ues {
				return "", false
			}
			return ues, true
		}
		inp.Next()
	}
}

Changes to input/input_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
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38







-
+








-
+







	"testing"

	"zettelstore.de/z/input"
)

func TestEatEOL(t *testing.T) {
	t.Parallel()
	inp := input.NewInput("")
	inp := input.NewInput(nil)
	inp.EatEOL()
	if inp.Ch != input.EOS {
		t.Errorf("No EOS found: %q", inp.Ch)
	}
	if inp.Pos != 0 {
		t.Errorf("Pos != 0: %d", inp.Pos)
	}

	inp = input.NewInput("ABC")
	inp = input.NewInput([]byte("ABC"))
	if inp.Ch != 'A' {
		t.Errorf("First ch != 'A', got %q", inp.Ch)
	}
	inp.EatEOL()
	if inp.Ch != 'A' {
		t.Errorf("First ch != 'A', got %q", inp.Ch)
	}
47
48
49
50
51
52
53
54

55
56
57
58
59
60
61
47
48
49
50
51
52
53

54
55
56
57
58
59
60
61







-
+







		{"", ""},
		{"a", ""},
		{"&amp;", "&"},
		{"&#9;", "\t"},
		{"&quot;", "\""},
	}
	for id, tc := range testcases {
		inp := input.NewInput(tc.text)
		inp := input.NewInput([]byte(tc.text))
		got, ok := inp.ScanEntity()
		if !ok {
			if tc.exp != "" {
				t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got)
			}
			if inp.Pos != 0 {
				t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos)

Changes to kernel/impl/box.go.

76
77
78
79
80
81
82
83

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

83
84
85
86
87
88
89
90







-
+







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

Changes to kernel/impl/cfg.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
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34






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








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














65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85







+














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










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






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








import (
	"context"
	"fmt"
	"strings"
	"sync"

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

type configService struct {
	srvConfig
	mxService sync.RWMutex
	rtConfig  *myConfig
}

func (cs *configService) Initialize() {
	cs.descr = descriptionMap{
		meta.KeyDefaultCopyright: {"Default copyright", parseString, true},
		meta.KeyDefaultLang:      {"Default language", parseString, true},
		meta.KeyDefaultRole:      {"Default role", parseString, true},
		meta.KeyDefaultSyntax:    {"Default syntax", parseString, true},
		meta.KeyDefaultTitle:     {"Default title", parseString, true},
		meta.KeyDefaultVisibility: {
		api.KeyDefaultCopyright: {"Default copyright", parseString, true},
		api.KeyDefaultLang:      {"Default language", parseString, true},
		api.KeyDefaultRole:      {"Default role", parseString, true},
		api.KeyDefaultSyntax:    {"Default syntax", parseString, true},
		api.KeyDefaultTitle:     {"Default title", parseString, true},
		api.KeyDefaultVisibility: {
			"Default zettel visibility",
			func(val string) interface{} {
				vis := meta.GetVisibility(val)
				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.KeyMarkerExternal:   {"Marker external URL", parseString, true},
		meta.KeyMaxTransclusions: {"Maximum transclusions", parseInt, true},
		meta.KeySiteName:         {"Site name", parseString, true},
		meta.KeyYAMLHeader:       {"YAML header", parseBool, true},
		meta.KeyZettelFileSyntax: {
		api.KeyExpertMode:       {"Expert mode", parseBool, true},
		api.KeyFooterHTML:       {"Footer HTML", parseString, true},
		api.KeyHomeZettel:       {"Home zettel", parseZid, true},
		api.KeyMarkerExternal:   {"Marker external URL", parseString, true},
		api.KeyMaxTransclusions: {"Maximum transclusions", parseInt, true},
		api.KeySiteName:         {"Site name", parseString, true},
		api.KeyYAMLHeader:       {"YAML header", parseBool, true},
		api.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.KeyMarkerExternal:    "&#10138;",
		meta.KeyMaxTransclusions:  1024,
		meta.KeySiteName:          "Zettelstore",
		meta.KeyYAMLHeader:        false,
		meta.KeyZettelFileSyntax:  nil,
		api.KeyDefaultCopyright:  "",
		api.KeyDefaultLang:       api.ValueLangEN,
		api.KeyDefaultRole:       api.ValueRoleZettel,
		api.KeyDefaultSyntax:     api.ValueSyntaxZmk,
		api.KeyDefaultTitle:      "Untitled",
		api.KeyDefaultVisibility: meta.VisibilityLogin,
		api.KeyExpertMode:        false,
		api.KeyFooterHTML:        "",
		api.KeyHomeZettel:        id.DefaultHomeZid,
		api.KeyMarkerExternal:    "&#10138;",
		api.KeyMaxTransclusions:  1024,
		api.KeySiteName:          "Zettelstore",
		api.KeyYAMLHeader:        false,
		api.KeyZettelFileSyntax:  nil,
	}
}

func (cs *configService) Start(kern *myKernel) error {
	kern.doLog("Start Config Service")
	data := meta.New(id.ConfigurationZid)
	for _, kv := range cs.GetNextConfigList() {
100
101
102
103
104
105
106
107

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

108
109
110
111
112
113
114
115







-
+







	kern.doLog("Stop Config Service")
	cs.mxService.Lock()
	cs.rtConfig = nil
	cs.mxService.Unlock()
	return nil
}

func (cs *configService) GetStatistics() []kernel.KeyValue {
func (*configService) GetStatistics() []kernel.KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {
	cs.rtConfig.setBox(mgr)
}

150
151
152
153
154
155
156
157
158
159
160
161
162






163
164
165
166
167
168
169
170
171
172
173
174

175
176
177
178
179
180
181
151
152
153
154
155
156
157






158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

175
176
177
178
179
180
181
182







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











-
+







func (cfg *myConfig) observe(ci box.UpdateInfo) {
	if ci.Reason == box.OnReload || ci.Zid == id.ConfigurationZid {
		go func() { cfg.doUpdate(ci.Box) }()
	}
}

var defaultKeys = map[string]string{
	meta.KeyCopyright: meta.KeyDefaultCopyright,
	meta.KeyLang:      meta.KeyDefaultLang,
	meta.KeyLicense:   meta.KeyDefaultLicense,
	meta.KeyRole:      meta.KeyDefaultRole,
	meta.KeySyntax:    meta.KeyDefaultSyntax,
	meta.KeyTitle:     meta.KeyDefaultTitle,
	api.KeyCopyright: api.KeyDefaultCopyright,
	api.KeyLang:      api.KeyDefaultLang,
	api.KeyLicense:   api.KeyDefaultLicense,
	api.KeyRole:      api.KeyDefaultRole,
	api.KeySyntax:    api.KeyDefaultSyntax,
	api.KeyTitle:     api.KeyDefaultTitle,
}

// AddDefaultValues enriches the given meta data with its default values.
func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta {
	if cfg == nil {
		return m
	}
	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 val, ok2 := cfg.data.Get(d); ok2 && val != "" {
				if result == m {
					result = m.Clone()
				}
				result.Set(k, val)
			}
		}
	}
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
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







-
+


-
+


-
+


-
+


-
+



-
+




-
+







-
+




-
+








-
+








-
+



-
+




-
+





-
+





-
+



-
+






	cfg.mx.RLock()
	val := cfg.data.GetBool(key)
	cfg.mx.RUnlock()
	return val
}

// GetDefaultTitle returns the current value of the "default-title" key.
func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(meta.KeyDefaultTitle) }
func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(api.KeyDefaultTitle) }

// GetDefaultRole returns the current value of the "default-role" key.
func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(meta.KeyDefaultRole) }
func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(api.KeyDefaultRole) }

// GetDefaultSyntax returns the current value of the "default-syntax" key.
func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(meta.KeyDefaultSyntax) }
func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(api.KeyDefaultSyntax) }

// GetDefaultLang returns the current value of the "default-lang" key.
func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(meta.KeyDefaultLang) }
func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(api.KeyDefaultLang) }

// GetSiteName returns the current value of the "site-name" key.
func (cfg *myConfig) GetSiteName() string { return cfg.getString(meta.KeySiteName) }
func (cfg *myConfig) GetSiteName() string { return cfg.getString(api.KeySiteName) }

// GetHomeZettel returns the value of the "home-zettel" key.
func (cfg *myConfig) GetHomeZettel() id.Zid {
	val := cfg.getString(meta.KeyHomeZettel)
	val := cfg.getString(api.KeyHomeZettel)
	if homeZid, err := id.Parse(val); err == nil {
		return homeZid
	}
	cfg.mx.RLock()
	val, _ = cfg.orig.Get(meta.KeyHomeZettel)
	val, _ = cfg.orig.Get(api.KeyHomeZettel)
	homeZid, _ := id.Parse(val)
	cfg.mx.RUnlock()
	return homeZid
}

// GetDefaultVisibility returns the default value for zettel visibility.
func (cfg *myConfig) GetDefaultVisibility() meta.Visibility {
	val := cfg.getString(meta.KeyDefaultVisibility)
	val := cfg.getString(api.KeyDefaultVisibility)
	if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
		return vis
	}
	cfg.mx.RLock()
	val, _ = cfg.orig.Get(meta.KeyDefaultVisibility)
	val, _ = cfg.orig.Get(api.KeyDefaultVisibility)
	vis := meta.GetVisibility(val)
	cfg.mx.RUnlock()
	return vis
}

// GetMaxTransclusions return the maximum number of indirect transclusions.
func (cfg *myConfig) GetMaxTransclusions() int {
	cfg.mx.RLock()
	val, ok := cfg.data.GetNumber(meta.KeyMaxTransclusions)
	val, ok := cfg.data.GetNumber(api.KeyMaxTransclusions)
	cfg.mx.RUnlock()
	if ok && val > 0 {
		return val
	}
	return 1024
}

// GetYAMLHeader returns the current value of the "yaml-header" key.
func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(meta.KeyYAMLHeader) }
func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(api.KeyYAMLHeader) }

// GetMarkerExternal returns the current value of the "marker-external" key.
func (cfg *myConfig) GetMarkerExternal() string {
	return cfg.getString(meta.KeyMarkerExternal)
	return cfg.getString(api.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) }
func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(api.KeyFooterHTML) }

// 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)
	return cfg.data.GetListOrNil(api.KeyZettelFileSyntax)
}

// --- AuthConfig

// GetExpertMode returns the current value of the "expert-mode" key
func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(meta.KeyExpertMode) }
func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(api.KeyExpertMode) }

// GetVisibility returns the visibility value, or "login" if none is given.
func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility {
	if val, ok := m.Get(meta.KeyVisibility); ok {
	if val, ok := m.Get(api.KeyVisibility); ok {
		if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
			return vis
		}
	}
	return cfg.GetDefaultVisibility()
}

Changes to kernel/impl/core.go.

37
38
39
40
41
42
43

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







+







	info  interface{}
	stack []byte
}

func (cs *coreService) Initialize() {
	cs.mapRecover = make(map[string]recoverInfo)
	cs.descr = descriptionMap{
		kernel.CoreDebug:     {"Debug mode", parseBool, false},
		kernel.CoreGoArch:    {"Go processor architecture", nil, false},
		kernel.CoreGoOS:      {"Go Operating System", nil, false},
		kernel.CoreGoVersion: {"Go Version", nil, false},
		kernel.CoreHostname:  {"Host name", nil, false},
		kernel.CorePort: {
			"Port of command line server",
			cs.noFrozen(func(val string) interface{} {
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

87
88
89
90
91
92
93
94







+












-
+







				}
				return val
			}),
			false,
		},
	}
	cs.next = interfaceMap{
		kernel.CoreDebug:     false,
		kernel.CoreGoArch:    runtime.GOARCH,
		kernel.CoreGoOS:      runtime.GOOS,
		kernel.CoreGoVersion: runtime.Version(),
		kernel.CoreHostname:  "*unknown host*",
		kernel.CorePort:      0,
		kernel.CoreVerbose:   false,
	}
	if hn, err := os.Hostname(); err == nil {
		cs.next[kernel.CoreHostname] = hn
	}
}

func (cs *coreService) Start(kern *myKernel) error {
func (cs *coreService) Start(*myKernel) error {
	cs.started = true
	return nil
}
func (cs *coreService) IsStarted() bool { return cs.started }
func (cs *coreService) Stop(*myKernel) error {
	cs.started = false
	return nil

Changes to kernel/impl/impl.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







-








// myKernel is the main internal kernel.
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

105
106
107
108
109
110
111
112

113
114
115
116
117
118
119
120
121
122
123
124
125
126





127
128
129
130
131
132
133
134
135
136
137
138
139
140

141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

172
173
174
175
176
177
178
179
180
181

182
183
184
185
186
187
188
104
105
106
107
108
109
110

111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143

144
145
146
147
148
149
150
151








152
153
154
155
156
157
158
159
160
161
162
163
164
165
166

167
168
169
170
171
172
173
174
175
176

177
178
179
180
181
182
183
184







-
+














+
+
+
+
+













-
+







-
-
-
-
-
-
-
-















-
+









-
+







	signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM)
	go func() {
		// Wait for interrupt.
		sig := <-kern.interrupt
		if strSig := sig.String(); strSig != "" {
			kern.doLog("Shut down Zettelstore:", strSig)
		}
		kern.shutdown()
		kern.doShutdown()
		kern.wg.Done()
	}()

	kern.StartService(kernel.CoreService)
	if headline {
		kern.doLog(fmt.Sprintf(
			"%v %v (%v@%v/%v)",
			kern.core.GetConfig(kernel.CoreProgname),
			kern.core.GetConfig(kernel.CoreVersion),
			kern.core.GetConfig(kernel.CoreGoVersion),
			kern.core.GetConfig(kernel.CoreGoOS),
			kern.core.GetConfig(kernel.CoreGoArch),
		))
		kern.doLog("Licensed under the latest version of the EUPL (European Union Public License)")
		if kern.core.GetConfig(kernel.CoreDebug).(bool) {
			kern.doLog("-------------------------------------------------")
			kern.doLog("WARNING: DEBUG MODE, DO NO USE THIS IN PRODUCTION")
			kern.doLog("-------------------------------------------------")
		}
		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)
		}
	}
}

func (kern *myKernel) shutdown() {
func (kern *myKernel) doShutdown() {
	kern.StopService(kernel.CoreService) // Will stop all other services.
}

func (kern *myKernel) WaitForShutdown() {
	kern.wg.Wait()
}

func (kern *myKernel) SetDebug(enable bool) bool {
	kern.mx.Lock()
	prevDebug := kern.debug
	kern.debug = enable
	kern.mx.Unlock()
	return prevDebug
}

// --- Shutdown operation ----------------------------------------------------

// Shutdown the service. Waits for all concurrent activity to stop.
func (kern *myKernel) Shutdown(silent bool) {
	kern.interrupt <- &shutdownSignal{silent: silent}
}

type shutdownSignal struct{ silent bool }

func (s *shutdownSignal) String() string {
	if s.silent {
		return ""
	}
	return "shutdown"
}
func (s *shutdownSignal) Signal() { /* Just a signal */ }
func (*shutdownSignal) Signal() { /* Just a signal */ }

// --- Log operation ---------------------------------------------------------

// Log some activity.
func (kern *myKernel) Log(args ...interface{}) {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	kern.doLog(args...)
}
func (kern *myKernel) doLog(args ...interface{}) {
func (*myKernel) doLog(args ...interface{}) {
	log.Println(args...)
}

// LogRecover outputs some information about the previous panic.
func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool {
	return kern.doLogRecover(name, recoverInfo)
}

Changes to kernel/impl/web.go.

44
45
46
47
48
49
50
51

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

51
52
53
54
55
56
57
58







-
+







		kernel.WebListenAddress: {
			"Listen address",
			func(val string) interface{} {
				host, port, err := net.SplitHostPort(val)
				if err != nil {
					return nil
				}
				if _, err := net.LookupPort("tcp", port); err != nil {
				if _, err = net.LookupPort("tcp", port); err != nil {
					return nil
				}
				return net.JoinHostPort(host, port)
			},
			true},
		kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true},
		kernel.WebSecureCookie:     {"Secure cookie", parseBool, true},
111
112
113
114
115
116
117
118

119
120
121

122
123
124
125
126
127
128
111
112
113
114
115
116
117

118
119
120

121
122
123
124
125
126
127
128







-
+


-
+








	srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, kern.cfg.rtConfig)
	if err != nil {
		kern.doLog("Unable to create Web Server:", err)
		return err
	}
	if kern.debug {
	if kern.core.GetConfig(kernel.CoreDebug).(bool) {
		srvw.SetDebug()
	}
	if err := srvw.Run(); err != nil {
	if err = srvw.Run(); err != nil {
		kern.doLog("Unable to start Web Service:", err)
		return err
	}
	kern.doLog("Start Web Service:", listenAddr)
	ws.mxService.Lock()
	ws.srvw = srvw
	ws.mxService.Unlock()

Changes to kernel/kernel.go.

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



33
34
35
36
37
38
39







-
-
-







type Kernel interface {
	// Start the service.
	Start(headline bool, lineServer bool)

	// WaitForShutdown blocks the call until Shutdown is called.
	WaitForShutdown()

	// SetDebug to enable/disable debug mode
	SetDebug(enable bool) bool

	// Shutdown the service. Waits for all concurrent activities to stop.
	Shutdown(silent bool)

	// Log some activity.
	Log(args ...interface{})

	// LogRecover outputs some information about the previous panic.
90
91
92
93
94
95
96

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







+







	AuthService
	BoxService
	WebService
)

// Constants for core service system keys.
const (
	CoreDebug     = "debug"
	CoreGoArch    = "go-arch"
	CoreGoOS      = "go-os"
	CoreGoVersion = "go-version"
	CoreHostname  = "hostname"
	CorePort      = "port"
	CoreProgname  = "progname"
	CoreVerbose   = "verbose"

Changes to parser/blob/blob.go.

1
2

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

15
16
17
18
19
20
21
1

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

-
+












+







//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
// 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 blob provides a parser of binary data.
package blob

import (
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

func init() {
45
46
47
48
49
50
51
52

53
54
55
56
57
58
59
60
61
62
63
64
46
47
48
49
50
51
52

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







-
+












	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) *ast.BlockListNode {
	if p := parser.Get(syntax); p != nil {
		syntax = p.Name
	}
	title, _ := m.Get(meta.KeyTitle)
	title, _ := m.Get(api.KeyTitle)
	return &ast.BlockListNode{List: []ast.BlockNode{
		&ast.BLOBNode{
			Title:  title,
			Syntax: syntax,
			Blob:   []byte(inp.Src),
		},
	}}
}

func parseInlines(*input.Input, string) *ast.InlineListNode {
	return nil
}

Changes to parser/cleaner/cleaner.go.

8
9
10
11
12
13
14

15
16
17
18

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

17

18
19
20
21
22
23
24
25







+

-

-
+







// under this license.
//-----------------------------------------------------------------------------

// Package cleaner provides funxtions to clean up the parsed AST.
package cleaner

import (
	"bytes"
	"strconv"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/strfun"
)

// CleanBlockList cleans the given block list.
func CleanBlockList(bln *ast.BlockListNode) { cleanNode(bln) }
60
61
62
63
64
65
66
67
68


69
70
71
72

73
74
75
76
77
78
79
60
61
62
63
64
65
66


67
68
69
70
71

72
73
74
75
76
77
78
79







-
-
+
+



-
+







}

func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || hn.Inlines.IsEmpty() {
		return
	}
	if hn.Slug == "" {
		var sb strings.Builder
		_, err := cv.textEnc.WriteInlines(&sb, hn.Inlines)
		var buf bytes.Buffer
		_, err := cv.textEnc.WriteInlines(&buf, hn.Inlines)
		if err != nil {
			return
		}
		hn.Slug = strfun.Slugify(sb.String())
		hn.Slug = strfun.Slugify(buf.String())
	}
	if hn.Slug != "" {
		hn.Fragment = cv.addIdentifier(hn.Slug, hn)
	}
}

func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) {
97
98
99
100
101
102
103
104

105
106
107
108
109
110
111
112
97
98
99
100
101
102
103

104
105
106
107
108
109
110
111
112







-
+








		cv.ids = map[string]ast.Node{id: node}
		return id
	}
	if n, ok := cv.ids[id]; ok && n != node {
		prefix := id + "-"
		for count := 1; ; count++ {
			newID := prefix + strconv.Itoa(count)
			if n, ok := cv.ids[newID]; !ok || n == node {
			if n2, ok2 := cv.ids[newID]; !ok2 || n2 == node {
				cv.ids[newID] = node
				return newID
			}
		}
	}
	cv.ids[id] = node
	return id
}

Changes to parser/markdown/markdown.go.

16
17
18
19
20
21
22
23

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

23
24
25
26
27
28
29
30







-
+







	"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/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

130
131
132
133
134
135
136
137

138
139
140
141
142
143
144
130
131
132
133
134
135
136

137
138
139
140
141
142
143
144







-
+







		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))
		attrs = attrs.Set("class", "language-"+cleanText(language, true))
	}
	return &ast.VerbatimNode{
		Kind:  ast.VerbatimProg,
		Attrs: attrs,
		Lines: p.acceptRawText(node),
	}
}
264
265
266
267
268
269
270
271

272
273
274
275
276
277
278
264
265
266
267
268
269
270

271
272
273
274
275
276
277
278







-
+







	if node.IsRaw() {
		return splitText(string(segment.Value(p.source)))
	}
	ins := splitText(string(segment.Value(p.source)))
	result := make([]ast.InlineNode, 0, len(ins)+1)
	for _, in := range ins {
		if tn, ok := in.(*ast.TextNode); ok {
			tn.Text = cleanText(tn.Text, true)
			tn.Text = cleanText([]byte(tn.Text), true)
		}
		result = append(result, in)
	}
	if node.HardLineBreak() {
		result = append(result, &ast.BreakNode{Hard: true})
	} else if node.SoftLineBreak() {
		result = append(result, &ast.BreakNode{Hard: false})
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
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







-
+

-
+





-
+

-
-
+
+





-
-
+
+



-
-
-

-
+

-
+







-
+




-
-
+
+



-
+


-
+

-
+




-
-
-
-
-
+
+

















-
+

-
+













-
+

-
+













-
-
+
+



-
-
+


-
+



-
+

-
-
+
+

-
+


-
+







	'-': true, '.': true, '/': true, ':': true, ';': true, '<': true,
	'=': true, '>': true, '?': true, '@': true, '[': true, '\\': true,
	']': true, '^': true, '_': true, '`': true, '{': true, '|': true,
	'}': true, '~': true,
}

// cleanText removes backslashes from TextNodes and expands entities
func cleanText(text string, cleanBS bool) string {
func cleanText(text []byte, cleanBS bool) string {
	lastPos := 0
	var sb strings.Builder
	var buf bytes.Buffer
	for pos, ch := range text {
		if pos < lastPos {
			continue
		}
		if ch == '&' {
			inp := input.NewInput(text[pos:])
			inp := input.NewInput([]byte(text[pos:]))
			if s, ok := inp.ScanEntity(); ok {
				sb.WriteString(text[lastPos:pos])
				sb.WriteString(s)
				buf.Write(text[lastPos:pos])
				buf.WriteString(s)
				lastPos = pos + inp.Pos
			}
			continue
		}
		if cleanBS && ch == '\\' && pos < len(text)-1 && ignoreAfterBS[text[pos+1]] {
			sb.WriteString(text[lastPos:pos])
			sb.WriteByte(text[pos+1])
			buf.Write(text[lastPos:pos])
			buf.WriteByte(text[pos+1])
			lastPos = pos + 2
		}
	}
	if lastPos == 0 {
		return text
	}
	if lastPos < len(text) {
		sb.WriteString(text[lastPos:])
		buf.Write(text[lastPos:])
	}
	return sb.String()
	return buf.String()
}

func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) []ast.InlineNode {
	return []ast.InlineNode{
		&ast.LiteralNode{
			Kind:  ast.LiteralProg,
			Attrs: nil, //TODO
			Text:  cleanCodeSpan(string(node.Text(p.source))),
			Text:  cleanCodeSpan(node.Text(p.source)),
		},
	}
}

func cleanCodeSpan(text string) string {
	if text == "" {
func cleanCodeSpan(text []byte) string {
	if len(text) == 0 {
		return ""
	}
	lastPos := 0
	var sb strings.Builder
	var buf bytes.Buffer
	for pos, ch := range text {
		if ch == '\n' {
			sb.WriteString(text[lastPos:pos])
			buf.Write(text[lastPos:pos])
			if pos < len(text)-1 {
				sb.WriteByte(' ')
				buf.WriteByte(' ')
			}
			lastPos = pos + 1
		}
	}
	if lastPos == 0 {
		return text
	}
	sb.WriteString(text[lastPos:])
	return sb.String()
	buf.Write(text[lastPos:])
	return buf.String()
}

func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) []ast.InlineNode {
	kind := ast.FormatEmph
	if node.Level == 2 {
		kind = ast.FormatStrong
	}
	return []ast.InlineNode{
		&ast.FormatNode{
			Kind:    kind,
			Attrs:   nil, //TODO
			Inlines: p.acceptInlineChildren(node),
		},
	}
}

func (p *mdP) acceptLink(node *gmAst.Link) []ast.InlineNode {
	ref := ast.ParseReference(cleanText(string(node.Destination), true))
	ref := ast.ParseReference(cleanText(node.Destination, true))
	var attrs *ast.Attributes
	if title := string(node.Title); len(title) > 0 {
	if title := node.Title; len(title) > 0 {
		attrs = attrs.Set("title", cleanText(title, true))
	}
	return []ast.InlineNode{
		&ast.LinkNode{
			Ref:     ref,
			Inlines: p.acceptInlineChildren(node),
			OnlyRef: false,
			Attrs:   attrs,
		},
	}
}

func (p *mdP) acceptImage(node *gmAst.Image) []ast.InlineNode {
	ref := ast.ParseReference(cleanText(string(node.Destination), true))
	ref := ast.ParseReference(cleanText(node.Destination, true))
	var attrs *ast.Attributes
	if title := string(node.Title); len(title) > 0 {
	if title := node.Title; len(title) > 0 {
		attrs = attrs.Set("title", cleanText(title, true))
	}
	return []ast.InlineNode{
		&ast.EmbedNode{
			Material: &ast.ReferenceMaterialNode{Ref: ref},
			Inlines:  p.flattenInlineList(node),
			Attrs:    attrs,
		},
	}
}

func (p *mdP) flattenInlineList(node gmAst.Node) *ast.InlineListNode {
	iln := p.acceptInlineChildren(node)
	var sb strings.Builder
	_, err := p.textEnc.WriteInlines(&sb, iln)
	var buf bytes.Buffer
	_, err := p.textEnc.WriteInlines(&buf, iln)
	if err != nil {
		panic(err)
	}
	text := sb.String()
	if text == "" {
	if buf.Len() == 0 {
		return nil
	}
	return ast.CreateInlineListNode(&ast.TextNode{Text: text})
	return ast.CreateInlineListNode(&ast.TextNode{Text: buf.String()})
}

func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) []ast.InlineNode {
	url := node.URL(p.source)
	u := node.URL(p.source)
	if node.AutoLinkType == gmAst.AutoLinkEmail &&
		!bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
		url = append([]byte("mailto:"), url...)
		!bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) {
		u = append([]byte("mailto:"), u...)
	}
	ref := ast.ParseReference(cleanText(string(url), false))
	ref := ast.ParseReference(cleanText(u, false))
	label := node.Label(p.source)
	if len(label) == 0 {
		label = url
		label = u
	}
	return []ast.InlineNode{
		&ast.LinkNode{
			Ref:     ref,
			Inlines: ast.CreateInlineListNode(&ast.TextNode{Text: string(label)}),
			OnlyRef: true,
			Attrs:   nil, //TODO

Changes to parser/markdown/markdown_test.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
8
9
10
11
12
13
14

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

35
36
37
38


39
40
41


42
43
44

45
46
47

48
49
50
51
52
53







-
+



















-
+



-
-
+
+

-
-
+
+

-
+


-
+





// under this license.
//-----------------------------------------------------------------------------

// Package markdown provides a parser for markdown.
package markdown

import (
	"strings"
	"bytes"
	"testing"

	"zettelstore.de/z/ast"
)

func TestSplitText(t *testing.T) {
	t.Parallel()
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"abc", "Tabc"},
		{" ", "S "},
		{"abc def", "TabcS Tdef"},
		{"abc def ", "TabcS TdefS "},
		{" abc def ", "S TabcS TdefS "},
	}
	for i, tc := range testcases {
		var sb strings.Builder
		var buf bytes.Buffer
		for _, in := range splitText(tc.text) {
			switch n := in.(type) {
			case *ast.TextNode:
				sb.WriteByte('T')
				sb.WriteString(n.Text)
				buf.WriteByte('T')
				buf.WriteString(n.Text)
			case *ast.SpaceNode:
				sb.WriteByte('S')
				sb.WriteString(n.Lexeme)
				buf.WriteByte('S')
				buf.WriteString(n.Lexeme)
			default:
				sb.WriteByte('Q')
				buf.WriteByte('Q')
			}
		}
		got := sb.String()
		got := buf.String()
		if tc.exp != got {
			t.Errorf("TC=%d, text=%q, exp=%q, got=%q", i, tc.text, tc.exp, got)
		}
	}
}

Changes to parser/none/none.go.

8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23

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

24
25
26
27
28
29
30
31







+








-
+







// under this license.
//-----------------------------------------------------------------------------

// Package none provides a none-parser for meta data.
package none

import (
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxNone,
		Name:          api.ValueSyntaxNone,
		AltNames:      []string{},
		IsTextParser:  false,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}
86
87
88
89
90
91
92
93

94
95
96
97
87
88
89
90
91
92
93

94
95
96
97
98







-
+




func parseInlines(inp *input.Input, _ string) *ast.InlineListNode {
	inp.SkipToEOL()
	return ast.CreateInlineListNode(
		&ast.FormatNode{
			Kind:  ast.FormatSpan,
			Attrs: &ast.Attributes{Attrs: map[string]string{"class": "warning"}},
			Inlines: ast.CreateInlineListNodeFromWords(
				"parser.meta.ParseInlines:", "not", "possible", "("+inp.Src[0:inp.Pos]+")",
				"parser.meta.ParseInlines:", "not", "possible", "("+string(inp.Src[0:inp.Pos])+")",
			),
		},
	)
}

Changes to parser/parser.go.

11
12
13
14
15
16
17

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







+







// Package parser provides a generic interface to a range of different parsers.
package parser

import (
	"fmt"
	"log"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser/cleaner"
)
36
37
38
39
40
41
42
43

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

44
45
46
47
48
49
50
51
52
53
54

55
56
57
58
59
60
61







-
+










-







	ParseBlocks   func(*input.Input, *meta.Meta, string) *ast.BlockListNode
	ParseInlines  func(*input.Input, string) *ast.InlineListNode
}

var registry = map[string]*Info{}

// Register the parser (info) for later retrieval.
func Register(pi *Info) *Info {
func Register(pi *Info) {
	if _, ok := registry[pi.Name]; ok {
		panic(fmt.Sprintf("Parser %q already registered", pi.Name))
	}
	registry[pi.Name] = pi
	for _, alt := range pi.AltNames {
		if _, ok := registry[alt]; ok {
			panic(fmt.Sprintf("Parser %q already registered", alt))
		}
		registry[alt] = pi
	}
	return pi
}

// GetSyntaxes returns a list of syntaxes implemented by all registered parsers.
func GetSyntaxes() []string {
	result := make([]string, 0, len(registry))
	for syntax := range registry {
		result = append(result, syntax)
104
105
106
107
108
109
110
111

112
113
114
115
116
117
118
119
120
121
122

123
124
125

126
127
128
129
130
131
132
133

134
135
104
105
106
107
108
109
110

111
112
113
114
115
116
117
118
119
120
121

122
123
124

125
126
127
128
129
130
131
132

133
134
135







-
+










-
+


-
+







-
+


func ParseInlines(inp *input.Input, syntax string) *ast.InlineListNode {
	return Get(syntax).ParseInlines(inp, syntax)
}

// ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice.
// Typically used to parse the title or other metadata of type Zettelmarkup.
func ParseMetadata(value string) *ast.InlineListNode {
	return ParseInlines(input.NewInput(value), meta.ValueSyntaxZmk)
	return ParseInlines(input.NewInput([]byte(value)), api.ValueSyntaxZmk)
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {
		inhMeta = rtConfig.AddDefaultValues(inhMeta)
	}
	if syntax == "" {
		syntax, _ = inhMeta.Get(meta.KeySyntax)
		syntax, _ = inhMeta.Get(api.KeySyntax)
	}
	parseMeta := inhMeta
	if syntax == meta.ValueSyntaxNone {
	if syntax == api.ValueSyntaxNone {
		parseMeta = m
	}
	return &ast.ZettelNode{
		Meta:    m,
		Content: zettel.Content,
		Zid:     m.Zid,
		InhMeta: inhMeta,
		Ast:     ParseBlocks(input.NewInput(zettel.Content.AsString()), parseMeta, syntax),
		Ast:     ParseBlocks(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax),
	}
}

Changes to parser/parser_test.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
24







-
+








// Package parser provides a generic interface to a range of different parsers.
package parser_test

import (
	"testing"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/c/api"
	"zettelstore.de/z/parser"

	_ "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.
39
40
41
42
43
44
45
46

47
48
49
50
51
52

53
54
55
56
57
58
59
39
40
41
42
43
44
45

46
47
48
49
50
51

52
53
54
55
56
57
58
59







-
+





-
+







		{"css", false, false},
		{"gif", false, true},
		{"jpeg", false, true},
		{"jpg", false, true},
		{"markdown", true, false},
		{"md", true, false},
		{"mustache", false, false},
		{meta.ValueSyntaxNone, false, false},
		{api.ValueSyntaxNone, false, false},
		{"plain", false, false},
		{"png", false, true},
		{"svg", false, true},
		{"text", false, false},
		{"txt", false, false},
		{meta.ValueSyntaxZmk, true, false},
		{api.ValueSyntaxZmk, true, false},
	}
	for _, tc := range testCases {
		delete(syntaxSet, tc.syntax)
		if got := parser.IsTextParser(tc.syntax); got != tc.text {
			t.Errorf("Syntax %q is text: %v, but got %v", tc.syntax, tc.text, got)
		}
		if got := parser.IsImageFormat(tc.syntax); got != tc.image {

Changes to parser/plain/plain.go.

83
84
85
86
87
88
89
90

91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112
83
84
85
86
87
88
89

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

105
106
107
108
109
110
111
112







-
+














-
+







	for {
		inp.EatEOL()
		posL := inp.Pos
		if inp.Ch == input.EOS {
			return lines
		}
		inp.SkipToEOL()
		lines = append(lines, inp.Src[posL:inp.Pos])
		lines = append(lines, string(inp.Src[posL:inp.Pos]))
	}
}

func parseInlines(inp *input.Input, syntax string) *ast.InlineListNode {
	return doParseInlines(inp, syntax, ast.LiteralProg)
}
func parseInlinesHTML(inp *input.Input, syntax string) *ast.InlineListNode {
	return doParseInlines(inp, syntax, ast.LiteralHTML)
}
func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) *ast.InlineListNode {
	inp.SkipToEOL()
	return ast.CreateInlineListNode(&ast.LiteralNode{
		Kind:  kind,
		Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}},
		Text:  inp.Src[0:inp.Pos],
		Text:  string(inp.Src[0:inp.Pos]),
	})
}

func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) *ast.BlockListNode {
	iln := parseSVGInlines(inp, syntax)
	if iln == nil {
		return nil
127
128
129
130
131
132
133
134

135
136
137
138
139
140
127
128
129
130
131
132
133

134
135
136
137
138
139
140







-
+






	})
}

func scanSVG(inp *input.Input) string {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	}
	svgSrc := inp.Src[inp.Pos:]
	svgSrc := string(inp.Src[inp.Pos:])
	if !strings.HasPrefix(svgSrc, "<svg ") {
		return ""
	}
	// TODO: check proper end </svg>
	return svgSrc
}

Changes to parser/zettelmark/block.go.

189
190
191
192
193
194
195
196

197
198
199
200
201
202
203
189
190
191
192
193
194
195

196
197
198
199
200
201
202
203







-
+







				return rn, true
			}
			inp.SetPos(posL)
		case input.EOS:
			return nil, false
		}
		inp.SkipToEOL()
		rn.Lines = append(rn.Lines, inp.Src[posL:inp.Pos])
		rn.Lines = append(rn.Lines, string(inp.Src[posL:inp.Pos]))
	}
}

var runeRegion = map[rune]ast.RegionKind{
	':': ast.RegionSpan,
	'<': ast.RegionQuote,
	'"': ast.RegionVerse,
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
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







-
-
+
+


-
-
-





+
+
+
-
+







	}

}

// parseHeading parses a head line.
func (cp *zmkP) parseHeading() (hn *ast.HeadingNode, success bool) {
	inp := cp.inp
	lvl := cp.countDelim(inp.Ch)
	if lvl < 3 {
	delims := cp.countDelim(inp.Ch)
	if delims < 3 {
		return nil, false
	}
	if lvl > 7 {
		lvl = 7
	}
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	if delims > 7 {
		delims = 7
	}
	hn = &ast.HeadingNode{Level: lvl - 1, Inlines: &ast.InlineListNode{}}
	hn = &ast.HeadingNode{Level: delims - 2, Inlines: &ast.InlineListNode{}}
	for {
		if input.IsEOLEOS(inp.Ch) {
			return hn, true
		}
		in := cp.parseInline()
		if in == nil {
			return hn, true
537
538
539
540
541
542
543
544

545
546
547
548
549
550
551
537
538
539
540
541
542
543

544
545
546
547
548
549
550
551







-
+







		return false
	}
	descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
	lbn := cp.descrl.Descriptions[defPos].Descriptions[descrPos]
	if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
		lpn.Inlines.Append(pn.Inlines.List...)
	} else {
		descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
		descrPos = len(cp.descrl.Descriptions[defPos].Descriptions) - 1
		cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn)
	}
	return true
}

// parseLinePara parses one line of inline material.
func (cp *zmkP) parseLinePara() *ast.ParaNode {

Changes to parser/zettelmark/inline.go.

8
9
10
11
12
13
14

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

17
18
19
20
21
22
23







+

-







// under this license.
//-----------------------------------------------------------------------------

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"bytes"
	"fmt"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
)

// parseInlineList parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineList() *ast.InlineListNode {
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80







-
+







			if inp.Ch == '{' {
				in, success = cp.parseEmbed()
			}
		case '#':
			return cp.parseTag()
		case '%':
			in, success = cp.parseComment()
		case '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':':
		case '/', '_', '*', '>', '~', '\'', '^', ',', '<', '"', ';', ':':
			in, success = cp.parseFormat()
		case '+', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '\\':
			return cp.parseBackslash()
		case '-':
			in, success = cp.parseNdash()
96
97
98
99
100
101
102
103
104


105
106
107
108
109
110
111
96
97
98
99
100
101
102


103
104
105
106
107
108
109
110
111







-
-
+
+







		return cp.parseTextBackslash()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '#', '%', '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':', '+', '`', runeModGrave, '=', '\\', '-', '&':
			return &ast.TextNode{Text: inp.Src[pos:inp.Pos]}
		case '/', input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '#', '%', '_', '*', '>', '~', '\'', '^', ',', '<', '"', ';', ':', '+', '`', runeModGrave, '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseTextBackslash() *ast.TextNode {
	cp.inp.Next()
	return cp.parseBackslashRest()
130
131
132
133
134
135
136
137

138
139
140
141
142
143
144
145
146
147
148

149
150
151
152
153
154
155
130
131
132
133
134
135
136

137
138
139
140
141
142
143
144
145
146
147

148
149
150
151
152
153
154
155







-
+










-
+







	}
	if inp.Ch == ' ' {
		inp.Next()
		return &ast.TextNode{Text: "\u00a0"}
	}
	pos := inp.Pos
	inp.Next()
	return &ast.TextNode{Text: inp.Src[pos:inp.Pos]}
	return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
}

func (cp *zmkP) parseSpace() *ast.SpaceNode {
	inp := cp.inp
	pos := inp.Pos
	for {
		inp.Next()
		switch inp.Ch {
		case ' ', '\t':
		default:
			return &ast.SpaceNode{Lexeme: inp.Src[pos:inp.Pos]}
			return &ast.SpaceNode{Lexeme: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseSoftBreak() *ast.BreakNode {
	cp.inp.EatEOL()
	return &ast.BreakNode{}
207
208
209
210
211
212
213
214

215
216
217
218
219
220
221
207
208
209
210
211
212
213

214
215
216
217
218
219
220
221







-
+







	}

	cp.skipSpace()
	pos = inp.Pos
	if !cp.readReferenceToClose(closeCh) {
		return "", nil, false
	}
	ref = inp.Src[pos:inp.Pos]
	ref = string(inp.Src[pos:inp.Pos])
	inp.Next()
	if inp.Ch != closeCh {
		return "", nil, false
	}
	inp.Next()
	if len(ins) == 0 {
		return ref, nil, true
302
303
304
305
306
307
308
309

310
311
312
313
314
315
316
302
303
304
305
306
307
308

309
310
311
312
313
314
315
316







-
+







		inp.Next()
	}
	ins, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := cp.parseAttributes(false)
	return &ast.CiteNode{Key: inp.Src[pos:posL], Inlines: ins, Attrs: attrs}, true
	return &ast.CiteNode{Key: string(inp.Src[pos:posL]), Inlines: ins, Attrs: attrs}, true
}

func (cp *zmkP) parseFootnote() (*ast.FootnoteNode, bool) {
	cp.inp.Next()
	iln, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
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
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







-
+













-
+

-
+















-
+






-
-
-
-
+
+
+
+
+







	pos := inp.Pos
	for inp.Ch != ']' {
		if !isNameRune(inp.Ch) {
			return nil, false
		}
		inp.Next()
	}
	mn := &ast.MarkNode{Text: inp.Src[pos:inp.Pos]}
	mn := &ast.MarkNode{Text: string(inp.Src[pos:inp.Pos])}
	inp.Next()
	return mn, true
}

func (cp *zmkP) parseTag() ast.InlineNode {
	inp := cp.inp
	posH := inp.Pos
	inp.Next()
	pos := inp.Pos
	for isNameRune(inp.Ch) {
		inp.Next()
	}
	if pos == inp.Pos || inp.Ch == '#' {
		return &ast.TextNode{Text: inp.Src[posH:inp.Pos]}
		return &ast.TextNode{Text: string(inp.Src[posH:inp.Pos])}
	}
	return &ast.TagNode{Tag: inp.Src[pos:inp.Pos]}
	return &ast.TagNode{Tag: string(inp.Src[pos:inp.Pos])}
}

func (cp *zmkP) parseComment() (res *ast.LiteralNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	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{Kind: ast.LiteralComment, Text: string(inp.Src[pos:inp.Pos])}, true
		}
		inp.Next()
	}
}

var mapRuneFormat = map[rune]ast.FormatKind{
	'/':  ast.FormatItalic,
	'*':  ast.FormatBold,
	'_':  ast.FormatUnder,
	'~':  ast.FormatStrike,
	'/':  ast.FormatEmphDeprecated,
	'_':  ast.FormatEmph,
	'*':  ast.FormatStrong,
	'>':  ast.FormatInsert,
	'~':  ast.FormatDelete,
	'\'': ast.FormatMonospace,
	'^':  ast.FormatSuper,
	',':  ast.FormatSub,
	'<':  ast.FormatQuotation,
	'"':  ast.FormatQuote,
	';':  ast.FormatSmall,
	':':  ast.FormatSpan,
442
443
444
445
446
447
448
449

450
451
452
453
454
455
456
443
444
445
446
447
448
449

450
451
452
453
454
455
456
457







-
+







			if inp.Ch == fch {
				inp.Next()
				fn.Attrs = cp.parseAttributes(false)
				return fn, true
			}
			fn.Inlines.Append(&ast.TextNode{Text: string(fch)})
		} else if in := cp.parseInline(); in != nil {
			if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
			if _, ok = in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
				return nil, false
			}
			fn.Inlines.Append(in)
		}
	}
}

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







-
+









-
+


-
+



-
+







	}
	inp.Next() // read 2nd formatting character
	if inp.Ch != fch {
		return nil, false
	}
	fn := &ast.LiteralNode{Kind: kind}
	inp.Next()
	var sb strings.Builder
	var buf bytes.Buffer
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Peek() == fch {
				inp.Next()
				inp.Next()
				fn.Attrs = cp.parseAttributes(false)
				fn.Text = sb.String()
				fn.Text = buf.String()
				return fn, true
			}
			sb.WriteRune(fch)
			buf.WriteRune(fch)
			inp.Next()
		} else {
			tn := cp.parseText()
			sb.WriteString(tn.Text)
			buf.WriteString(tn.Text)
		}
	}
}

func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) {
	inp := cp.inp
	if inp.Peek() != inp.Ch {

Changes to parser/zettelmark/post-processor.go.

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

66
67
68
69
70
71
72







-







	case *ast.EmbedNode:
		return pp
	case *ast.CiteNode:
		return pp
	case *ast.FootnoteNode:
		return pp
	case *ast.FormatNode:
		pp.visitFormat(n)
		return pp
	}
	return nil
}

func (pp *postProcessor) visitRegion(rn *ast.RegionNode) {
	oldVerse := pp.inVerse
108
109
110
111
112
113
114
115

116
117
118
119
120
121
122
107
108
109
110
111
112
113

114
115
116
117
118
119
120
121







-
+







		for i, cell := range tn.Header {
			pp.processCell(cell, tn.Align[i])
		}
	}
	pp.visitTableRows(tn, width)
}

func (pp *postProcessor) visitTableHeader(tn *ast.TableNode) {
func (*postProcessor) visitTableHeader(tn *ast.TableNode) {
	for pos, cell := range tn.Header {
		ins := cell.Inlines.List
		if len(ins) == 0 {
			continue
		}
		if textNode, ok := ins[0].(*ast.TextNode); ok {
			textNode.Text = strings.TrimPrefix(textNode.Text, "=")
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
213
214
215
216
217
218
219
















220
221
222
223
224
225
226







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







	}
	if tn, ok := ins[0].(*ast.TextNode); ok && len(tn.Text) > 0 {
		return tn
	}
	return nil
}

var mapSemantic = map[ast.FormatKind]ast.FormatKind{
	ast.FormatItalic: ast.FormatEmph,
	ast.FormatBold:   ast.FormatStrong,
	ast.FormatUnder:  ast.FormatInsert,
	ast.FormatStrike: ast.FormatDelete,
}

func (pp *postProcessor) visitFormat(fn *ast.FormatNode) {
	if fn.Attrs.HasDefault() {
		if newKind, ok := mapSemantic[fn.Kind]; ok {
			fn.Attrs.RemoveDefault()
			fn.Kind = newKind
		}
	}
}

func (pp *postProcessor) visitBlockList(bln *ast.BlockListNode) {
	if bln == nil {
		return
	}
	if len(bln.List) == 0 {
		bln.List = nil
		return
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
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







-
-
-
-
+
-
-
-
-

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










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

-
+


















-
+










	again := false
	fromPos, toPos := 0, 0
	for fromPos < maxPos {
		ins[toPos] = ins[fromPos]
		fromPos++
		switch in := ins[toPos].(type) {
		case *ast.TextNode:
			for fromPos < maxPos {
				if tn, ok := ins[fromPos].(*ast.TextNode); ok {
					in.Text = in.Text + tn.Text
					fromPos++
			fromPos = processTextNode(ins, maxPos, in, fromPos)
				} else {
					break
				}
			}
		case *ast.SpaceNode:
			if fromPos < maxPos {
			again, fromPos = pp.processSpaceNode(ins, maxPos, in, toPos, again, fromPos)
				switch nn := ins[fromPos].(type) {
				case *ast.BreakNode:
					if len(in.Lexeme) > 1 {
						nn.Hard = true
						ins[toPos] = nn
						fromPos++
					}
				case *ast.TextNode:
					if pp.inVerse {
						ins[toPos] = &ast.TextNode{Text: strings.Repeat("\u00a0", len(in.Lexeme)) + nn.Text}
						fromPos++
						again = true
					}
				}
			}
		case *ast.BreakNode:
			if pp.inVerse {
				in.Hard = true
			}
		}
		toPos++
	}
	return again, toPos
}

func processTextNode(ins []ast.InlineNode, maxPos int, in *ast.TextNode, fromPos int) int {
	for fromPos < maxPos {
		if tn, ok := ins[fromPos].(*ast.TextNode); ok {
			in.Text = in.Text + tn.Text
			fromPos++
		} else {
			break
		}
	}
	return fromPos
}

func (pp *postProcessor) processSpaceNode(
	ins []ast.InlineNode, maxPos int, in *ast.SpaceNode, toPos int, again bool, fromPos int,
) (bool, int) {
	if fromPos < maxPos {
		switch nn := ins[fromPos].(type) {
		case *ast.BreakNode:
			if len(in.Lexeme) > 1 {
				nn.Hard = true
				ins[toPos] = nn
				fromPos++
			}
		case *ast.TextNode:
			if pp.inVerse {
				ins[toPos] = &ast.TextNode{Text: strings.Repeat("\u00a0", len(in.Lexeme)) + nn.Text}
				fromPos++
				again = true
			}
		case *ast.LiteralNode:
			if nn.Kind == ast.LiteralComment {
				ins[toPos] = ins[fromPos]
				fromPos++
			}
		}
	}
	return again, fromPos
}

// processInlineSliceTail removes empty text nodes, breaks and spaces at the end.
func (pp *postProcessor) processInlineSliceTail(iln *ast.InlineListNode, toPos int) int {
func (*postProcessor) processInlineSliceTail(iln *ast.InlineListNode, toPos int) int {
	ins := iln.List
	for toPos > 0 {
		switch n := ins[toPos-1].(type) {
		case *ast.TextNode:
			if len(n.Text) > 0 {
				return toPos
			}
		case *ast.BreakNode:
		case *ast.SpaceNode:
		default:
			return toPos
		}
		toPos--
		ins[toPos] = nil // Kill node to enable garbage collection
	}
	return toPos
}

func (pp *postProcessor) processInlineListInplace(iln *ast.InlineListNode) {
func (*postProcessor) processInlineListInplace(iln *ast.InlineListNode) {
	for _, in := range iln.List {
		if n, ok := in.(*ast.TextNode); ok {
			if n.Text == "..." {
				n.Text = "\u2026"
			} else if len(n.Text) == 4 && strings.IndexByte(",;:!?", n.Text[3]) >= 0 && n.Text[:3] == "..." {
				n.Text = "\u2026" + n.Text[3:]
			}
		}
	}
}

Changes to parser/zettelmark/zettelmark.go.

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







+








-
+








// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"unicode"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxZmk,
		Name:          api.ValueSyntaxZmk,
		AltNames:      nil,
		IsTextParser:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}
102
103
104
105
106
107
108
109

110
111
112
113
114
115
116
103
104
105
106
107
108
109

110
111
112
113
114
115
116
117







-
+







			return false
		case '\n', '\r':
			if sameLine {
				return false
			}
			fallthrough
		case ' ', '}':
			updateAttrs(attrs, key, inp.Src[posV:inp.Pos])
			updateAttrs(attrs, key, string(inp.Src[posV:inp.Pos]))
			return true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string, sameLine bool) bool {
164
165
166
167
168
169
170
171

172
173
174
175
176
177
178
165
166
167
168
169
170
171

172
173
174
175
176
177
178
179







-
+







	inp := cp.inp
	if sameLine {
		pos := inp.Pos
		for isNameRune(inp.Ch) {
			inp.Next()
		}
		if pos < inp.Pos {
			return &ast.Attributes{Attrs: map[string]string{"": inp.Src[pos:inp.Pos]}}
			return &ast.Attributes{Attrs: map[string]string{"": string(inp.Src[pos:inp.Pos])}}
		}

		// No immediate name: skip spaces
		cp.skipSpace()
	}

	pos := inp.Pos
212
213
214
215
216
217
218
219

220
221
222
223
224
225
226
213
214
215
216
217
218
219

220
221
222
223
224
225
226
227







-
+







			posC := inp.Pos
			for isNameRune(inp.Ch) {
				inp.Next()
			}
			if posC == inp.Pos {
				return false
			}
			updateAttrs(attrs, "class", inp.Src[posC:inp.Pos])
			updateAttrs(attrs, "class", string(inp.Src[posC:inp.Pos]))
		case '=':
			delete(attrs, "")
			if !cp.parseAttributeValue("", attrs, sameLine) {
				return false
			}
		default:
			if !cp.parseNormalAttribute(attrs, sameLine) {

Changes to parser/zettelmark/zettelmark_test.go.

8
9
10
11
12
13
14

15
16
17
18
19
20
21


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


21
22
23
24
25
26
27
28
29







+





-
-
+
+







// under this license.
//-----------------------------------------------------------------------------

// Package zettelmark_test provides some tests for the zettelmarkup parser.
package zettelmark_test

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

	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"

	// Ensure that the text encoder is available.
	// Needed by parser/cleanup.go
	_ "zettelstore.de/z/encoder/textenc"
)
43
44
45
46
47
48
49
50
51


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


51
52
53
54
55
56
57
58
59







-
-
+
+








func checkTcs(t *testing.T, tcs TestCases) {
	t.Helper()

	for tcn, tc := range tcs {
		t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) {
			st.Helper()
			inp := input.NewInput(tc.source)
			bns := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk)
			inp := input.NewInput([]byte(tc.source))
			bns := parser.ParseBlocks(inp, nil, api.ValueSyntaxZmk)
			var tv TestVisitor
			ast.Walk(&tv, bns)
			got := tv.String()
			if tc.want != got {
				st.Errorf("\nwant=%q\n got=%q", tc.want, got)
			}
		})
93
94
95
96
97
98
99

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







+







		{"...,", "(PARA \u2026,)"},
		{"...;", "(PARA \u2026;)"},
		{"...:", "(PARA \u2026:)"},
		{"...!", "(PARA \u2026!)"},
		{"...?", "(PARA \u2026?)"},
		{"...-", "(PARA ...-)"},
		{"a...b", "(PARA a...b)"},
		// {"http://a, http://b", "(PARA http://a, SP http://b)"},
	})
}

func TestSpace(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{" ", ""},
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
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







-
+








+
-
+





+
+
+
+
















-
-
-
+
+
+







		{"%%a", "(PARA {% a})"},
		{"%%%a", "(PARA {% a})"},
		{"%% a", "(PARA {% a})"},
		{"%%%  a", "(PARA {% a})"},
		{"%% % a", "(PARA {% % a})"},
		{"%%a", "(PARA {% a})"},
		{"a%%b", "(PARA a {% b})"},
		{"a %%b", "(PARA a SP {% b})"},
		{"a %%b", "(PARA a {% b})"},
		{" %%b", "(PARA {% b})"},
		{"%%b ", "(PARA {% b })"},
		{"100%", "(PARA 100%)"},
	})
}

func TestFormat(t *testing.T) {
	t.Parallel()
	// Not for Insert / '>', because collision with quoted list
	for _, ch := range []string{"/", "*", "_", "~", "'", "^", ",", "<", "\"", ";", ":"} {
	for _, ch := range []string{"_", "*", "~", "'", "^", ",", "<", "\"", ";", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
		}))
	}
	for _, ch := range []string{"_", "*", ">", "~", "'", "^", ",", "<", "\"", ";", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},
			{"$$\\$", "(PARA $$$)"},
			{"$$a\\$$", "(PARA $$a$$)"},
			{"$$a$\\$", "(PARA $$a$$)"},
			{"$$a\\$$$", "(PARA {$ a$})"},
			{"$$a\na$$", "(PARA {$ a SB a})"},
			{"$$a\n\na$$", "(PARA $$a)(PARA a$$)"},
			{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
		}))
	}
	checkTcs(t, TestCases{
		{"//****//", "(PARA {/ {*}})"},
		{"//**a**//", "(PARA {/ {* a}})"},
		{"//**//**", "(PARA // {* //})"},
		{"__****__", "(PARA {_ {*}})"},
		{"__**a**__", "(PARA {_ {* a}})"},
		{"__**__**", "(PARA __ {* __})"},
	})
}

func TestLiteral(t *testing.T) {
	t.Parallel()
	for _, ch := range []string{"`", "+", "="} {
		checkTcs(t, replace(ch, TestCases{
357
358
359
360
361
362
363
364

365
366
367


368
369
370
371
372
373
374
364
365
366
367
368
369
370

371
372


373
374
375
376
377
378
379
380
381







-
+

-
-
+
+







		{"++\\+++", "(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})"},
		{"//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{
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
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







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

-
+



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







	t.Parallel()
	checkTcs(t, TestCases{
		{"=h", "(PARA =h)"},
		{"= h", "(PARA = SP h)"},
		{"==h", "(PARA ==h)"},
		{"== h", "(PARA == SP h)"},
		{"===h", "(PARA ===h)"},
		{"=== h", "(H2 h #h)"},
		{"===  h", "(H2 h #h)"},
		{"==== h", "(H3 h #h)"},
		{"===== h", "(H4 h #h)"},
		{"====== h", "(H5 h #h)"},
		{"======= h", "(H6 h #h)"},
		{"======== h", "(H6 h #h)"},
		{"=== h", "(H1 h #h)"},
		{"===  h", "(H1 h #h)"},
		{"==== h", "(H2 h #h)"},
		{"===== h", "(H3 h #h)"},
		{"====== h", "(H4 h #h)"},
		{"======= h", "(H5 h #h)"},
		{"======== h", "(H5 h #h)"},
		{"=", "(PARA =)"},
		{"=== h=//=a//", "(H2 h= {/ =a} #h-a)"},
		{"=== h=__=a__", "(H1 h= {_ =a} #h-a)"},
		{"=\n", "(PARA =)"},
		{"a=", "(PARA a=)"},
		{" =", "(PARA =)"},
		{"=== h\na", "(H2 h #h)(PARA a)"},
		{"=== h i {-}", "(H2 h SP i #h-i)[ATTR -]"},
		{"=== h {{a}}", "(H2 h SP (EMBED a) #h)"},
		{"=== h{{a}}", "(H2 h (EMBED a) #h)"},
		{"=== {{a}}", "(H2 (EMBED a))"},
		{"=== h {{a}}{-}", "(H2 h SP (EMBED a)[ATTR -] #h)"},
		{"=== h {{a}} {-}", "(H2 h SP (EMBED a) #h)[ATTR -]"},
		{"=== h {-}{{a}}", "(H2 h #h)[ATTR -]"},
		{"=== h{id=abc}", "(H2 h #h)[ATTR id=abc]"},
		{"=== h\n=== h", "(H2 h #h)(H2 h #h-1)"},
		{"=== h\na", "(H1 h #h)(PARA a)"},
		{"=== h i {-}", "(H1 h SP i #h-i)[ATTR -]"},
		{"=== h {{a}}", "(H1 h SP (EMBED a) #h)"},
		{"=== h{{a}}", "(H1 h (EMBED a) #h)"},
		{"=== {{a}}", "(H1 (EMBED a))"},
		{"=== h {{a}}{-}", "(H1 h SP (EMBED a)[ATTR -] #h)"},
		{"=== h {{a}} {-}", "(H1 h SP (EMBED a) #h)[ATTR -]"},
		{"=== h {-}{{a}}", "(H1 h #h)[ATTR -]"},
		{"=== h{id=abc}", "(H1 h #h)[ATTR id=abc]"},
		{"=== h\n=== h", "(H1 h #h)(H1 h #h-1)"},
	})
}

func TestHRule(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"-", "(PARA -)"},
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
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







-
+


-
+






-
+

-
+





-
+

-
-
+
+

-
+






-
+

-
+



-
+

-
+

-
+


-
+


-
-
+
+

-
+


-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+


-
+

-
+

-
+

-
-
+
+

-
+

-
+


-
+

-
+


-
+

-
-
+
+

-
+

-
+


-
+

-
-
-
+
+
+

-
+

-
-
-
+
+
+


-
+

-
+



-
+

-
+


-
+

-
+




-
+



-
+







-
+



-
+


-
+

-
+


-
+

-
-
-
+
+
+


-
-
+
+

-
+

-
+

-
+






-
-
+
+

-
-
+
+

-
+







	})
}

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

// TestVisitor serializes the abstract syntax tree to a string.
type TestVisitor struct {
	b strings.Builder
	buf bytes.Buffer
}

func (tv *TestVisitor) String() string { return tv.b.String() }
func (tv *TestVisitor) String() string { return tv.buf.String() }

func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineListNode:
		tv.visitInlineList(n)
	case *ast.ParaNode:
		tv.b.WriteString("(PARA")
		tv.buf.WriteString("(PARA")
		ast.Walk(tv, n.Inlines)
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
	case *ast.VerbatimNode:
		code, ok := mapVerbatimKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind))
		}
		tv.b.WriteString(code)
		tv.buf.WriteString(code)
		for _, line := range n.Lines {
			tv.b.WriteByte('\n')
			tv.b.WriteString(line)
			tv.buf.WriteByte('\n')
			tv.buf.WriteString(line)
		}
		tv.b.WriteByte(')')
		tv.buf.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)
		tv.buf.WriteString(code)
		if n.Blocks != nil && len(n.Blocks.List) > 0 {
			tv.b.WriteByte(' ')
			tv.buf.WriteByte(' ')
			ast.Walk(tv, n.Blocks)
		}
		if n.Inlines != nil {
			tv.b.WriteString(" (LINE")
			tv.buf.WriteString(" (LINE")
			ast.Walk(tv, n.Inlines)
			tv.b.WriteByte(')')
			tv.buf.WriteByte(')')
		}
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HeadingNode:
		fmt.Fprintf(&tv.b, "(H%d", n.Level)
		fmt.Fprintf(&tv.buf, "(H%d", n.Level)
		ast.Walk(tv, n.Inlines)
		if n.Fragment != "" {
			tv.b.WriteString(" #")
			tv.b.WriteString(n.Fragment)
			tv.buf.WriteString(" #")
			tv.buf.WriteString(n.Fragment)
		}
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HRuleNode:
		tv.b.WriteString("(HR)")
		tv.buf.WriteString("(HR)")
		tv.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		tv.b.WriteString(mapNestedListKind[n.Kind])
		tv.buf.WriteString(mapNestedListKind[n.Kind])
		for _, item := range n.Items {
			tv.b.WriteString(" {")
			tv.buf.WriteString(" {")
			ast.WalkItemSlice(tv, item)
			tv.b.WriteByte('}')
			tv.buf.WriteByte('}')
		}
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
	case *ast.DescriptionListNode:
		tv.b.WriteString("(DL")
		tv.buf.WriteString("(DL")
		for _, def := range n.Descriptions {
			tv.b.WriteString(" (DT")
			tv.buf.WriteString(" (DT")
			ast.Walk(tv, def.Term)
			tv.b.WriteByte(')')
			tv.buf.WriteByte(')')
			for _, b := range def.Descriptions {
				tv.b.WriteString(" (DD ")
				tv.buf.WriteString(" (DD ")
				ast.WalkDescriptionSlice(tv, b)
				tv.b.WriteByte(')')
				tv.buf.WriteByte(')')
			}
		}
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
	case *ast.TableNode:
		tv.b.WriteString("(TAB")
		tv.buf.WriteString("(TAB")
		if len(n.Header) > 0 {
			tv.b.WriteString(" (TR")
			tv.buf.WriteString(" (TR")
			for _, cell := range n.Header {
				tv.b.WriteString(" (TH")
				tv.b.WriteString(alignString[cell.Align])
				tv.buf.WriteString(" (TH")
				tv.buf.WriteString(alignString[cell.Align])
				ast.Walk(tv, cell.Inlines)
				tv.b.WriteString(")")
				tv.buf.WriteString(")")
			}
			tv.b.WriteString(")")
			tv.buf.WriteString(")")
		}
		if len(n.Rows) > 0 {
			tv.b.WriteString(" ")
			tv.buf.WriteString(" ")
			for _, row := range n.Rows {
				tv.b.WriteString("(TR")
				tv.buf.WriteString("(TR")
				for i, cell := range row {
					if i == 0 {
						tv.b.WriteString(" ")
						tv.buf.WriteString(" ")
					}
					tv.b.WriteString("(TD")
					tv.b.WriteString(alignString[cell.Align])
					tv.buf.WriteString("(TD")
					tv.buf.WriteString(alignString[cell.Align])
					ast.Walk(tv, cell.Inlines)
					tv.b.WriteString(")")
					tv.buf.WriteString(")")
				}
				tv.b.WriteString(")")
				tv.buf.WriteString(")")
			}
		}
		tv.b.WriteString(")")
		tv.buf.WriteString(")")
	case *ast.BLOBNode:
		tv.b.WriteString("(BLOB ")
		tv.b.WriteString(n.Syntax)
		tv.b.WriteString(")")
		tv.buf.WriteString("(BLOB ")
		tv.buf.WriteString(n.Syntax)
		tv.buf.WriteString(")")
	case *ast.TextNode:
		tv.b.WriteString(n.Text)
		tv.buf.WriteString(n.Text)
	case *ast.TagNode:
		tv.b.WriteByte('#')
		tv.b.WriteString(n.Tag)
		tv.b.WriteByte('#')
		tv.buf.WriteByte('#')
		tv.buf.WriteString(n.Tag)
		tv.buf.WriteByte('#')
	case *ast.SpaceNode:
		if len(n.Lexeme) == 1 {
			tv.b.WriteString("SP")
			tv.buf.WriteString("SP")
		} else {
			fmt.Fprintf(&tv.b, "SP%d", len(n.Lexeme))
			fmt.Fprintf(&tv.buf, "SP%d", len(n.Lexeme))
		}
	case *ast.BreakNode:
		if n.Hard {
			tv.b.WriteString("HB")
			tv.buf.WriteString("HB")
		} else {
			tv.b.WriteString("SB")
			tv.buf.WriteString("SB")
		}
	case *ast.LinkNode:
		fmt.Fprintf(&tv.b, "(LINK %v", n.Ref)
		fmt.Fprintf(&tv.buf, "(LINK %v", n.Ref)
		ast.Walk(tv, n.Inlines)
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedNode:
		switch m := n.Material.(type) {
		case *ast.ReferenceMaterialNode:
			fmt.Fprintf(&tv.b, "(EMBED %v", m.Ref)
			fmt.Fprintf(&tv.buf, "(EMBED %v", m.Ref)
			if n.Inlines != nil {
				ast.Walk(tv, n.Inlines)
			}
			tv.b.WriteByte(')')
			tv.buf.WriteByte(')')
			tv.visitAttributes(n.Attrs)
		case *ast.BLOBMaterialNode:
			panic("TODO: zmktest blob")
		default:
			panic(fmt.Sprintf("Unknown material type %t for %v", n.Material, n.Material))
		}
	case *ast.CiteNode:
		fmt.Fprintf(&tv.b, "(CITE %s", n.Key)
		fmt.Fprintf(&tv.buf, "(CITE %s", n.Key)
		if n.Inlines != nil {
			ast.Walk(tv, n.Inlines)
		}
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.FootnoteNode:
		tv.b.WriteString("(FN")
		tv.buf.WriteString("(FN")
		ast.Walk(tv, n.Inlines)
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		tv.b.WriteString("(MARK")
		tv.buf.WriteString("(MARK")
		if n.Text != "" {
			tv.b.WriteString(" \"")
			tv.b.WriteString(n.Text)
			tv.b.WriteByte('"')
			tv.buf.WriteString(" \"")
			tv.buf.WriteString(n.Text)
			tv.buf.WriteByte('"')
		}
		if n.Fragment != "" {
			tv.b.WriteString(" #")
			tv.b.WriteString(n.Fragment)
			tv.buf.WriteString(" #")
			tv.buf.WriteString(n.Fragment)
		}
		tv.b.WriteByte(')')
		tv.buf.WriteByte(')')
	case *ast.FormatNode:
		fmt.Fprintf(&tv.b, "{%c", mapFormatKind[n.Kind])
		fmt.Fprintf(&tv.buf, "{%c", mapFormatKind[n.Kind])
		ast.Walk(tv, n.Inlines)
		tv.b.WriteByte('}')
		tv.buf.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)
		tv.buf.WriteByte('{')
		tv.buf.WriteRune(code)
		if n.Text != "" {
			tv.b.WriteByte(' ')
			tv.b.WriteString(n.Text)
			tv.buf.WriteByte(' ')
			tv.buf.WriteString(n.Text)
		}
		tv.b.WriteByte('}')
		tv.buf.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	default:
		return tv
	}
	return nil
}

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







-
-
-
-
+
+
+
+


















-
+








-
+








-
-
+
+


-
+

-
-
-
+
+
+

-
+




-
+

	ast.AlignDefault: "",
	ast.AlignLeft:    "l",
	ast.AlignCenter:  "c",
	ast.AlignRight:   "r",
}

var mapFormatKind = map[ast.FormatKind]rune{
	ast.FormatItalic:    '/',
	ast.FormatBold:      '*',
	ast.FormatUnder:     '_',
	ast.FormatStrike:    '~',
	ast.FormatEmph:      '_',
	ast.FormatStrong:    '*',
	ast.FormatInsert:    '>',
	ast.FormatDelete:    '~',
	ast.FormatMonospace: '\'',
	ast.FormatSuper:     '^',
	ast.FormatSub:       ',',
	ast.FormatQuote:     '"',
	ast.FormatQuotation: '<',
	ast.FormatSmall:     ';',
	ast.FormatSpan:      ':',
}

var mapLiteralKind = map[ast.LiteralKind]rune{
	ast.LiteralProg:    '`',
	ast.LiteralKeyb:    '+',
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
}

func (tv *TestVisitor) visitInlineList(iln *ast.InlineListNode) {
	for _, in := range iln.List {
		tv.b.WriteByte(' ')
		tv.buf.WriteByte(' ')
		ast.Walk(tv, in)
	}
}

func (tv *TestVisitor) visitAttributes(a *ast.Attributes) {
	if a.IsEmpty() {
		return
	}
	tv.b.WriteString("[ATTR")
	tv.buf.WriteString("[ATTR")

	keys := make([]string, 0, len(a.Attrs))
	for k := range a.Attrs {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		tv.b.WriteByte(' ')
		tv.b.WriteString(k)
		tv.buf.WriteByte(' ')
		tv.buf.WriteString(k)
		v := a.Attrs[k]
		if len(v) > 0 {
			tv.b.WriteByte('=')
			tv.buf.WriteByte('=')
			if strings.ContainsRune(v, ' ') {
				tv.b.WriteByte('"')
				tv.b.WriteString(v)
				tv.b.WriteByte('"')
				tv.buf.WriteByte('"')
				tv.buf.WriteString(v)
				tv.buf.WriteByte('"')
			} else {
				tv.b.WriteString(v)
				tv.buf.WriteString(v)
			}
		}
	}

	tv.b.WriteByte(']')
	tv.buf.WriteByte(']')
}

Changes to search/print.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
26







-
+







package search

import (
	"io"
	"sort"
	"strconv"

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

// Print the search to a writer.
func (s *Search) Print(w io.Writer) {
	if s.negate {
		io.WriteString(w, "NOT (")
	}
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







-
+







	if s.negate {
		io.WriteString(w, ")")
		space = true
	}

	if ord := s.order; len(ord) > 0 {
		switch ord {
		case meta.KeyID:
		case api.KeyID:
			// Ignore
		case RandomOrder:
			space = printSpace(w, space)
			io.WriteString(w, "RANDOM")
		default:
			space = printSpace(w, space)
			io.WriteString(w, "SORT ")

Changes to search/search.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 (
	"math/rand"
	"sort"
	"strings"
	"sync"

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

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {
	// Select all zettel that contains the given exact word.
228
229
230
231
232
233
234
235

236
237
238
239

240
241
242
243
244
245
246
229
230
231
232
233
234
235

236
237
238
239

240
241
242
243
244
245
246
247







-
+



-
+







func (s *Search) EnrichNeeded() bool {
	if s == nil {
		return false
	}
	s.mx.RLock()
	defer s.mx.RUnlock()
	for key := range s.tags {
		if meta.IsComputed(key) || key == meta.KeyTags {
		if meta.IsComputed(key) || key == api.KeyTags {
			return true
		}
	}
	if order := s.order; order != "" && (meta.IsComputed(order) || order == meta.KeyTags) {
	if order := s.order; order != "" && (meta.IsComputed(order) || order == api.KeyTags) {
		return true
	}
	return false
}

// CompileMatch returns a function to match meta data based on select specification.
func (s *Search) CompileMatch(searcher Searcher) MetaMatchFunc {
254
255
256
257
258
259
260
261

262
263
264
265
266
267
268
255
256
257
258
259
260
261

262
263
264
265
266
267
268
269







-
+







	compSearch := compileFullSearch(searcher, 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 selectNone(*meta.Meta) bool { return true }

func compilePreMatch(preMatch, compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc {
	if compMeta == nil {
		if compSearch == nil {
			return preMatch
		}
		if negate {
315
316
317
318
319
320
321
322

323
324
325
326
327
328
329
316
317
318
319
320
321
322

323
324
325
326
327
328
329
330







-
+








	if s == nil {
		sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
		return metaList
	}

	if s.order == "" {
		sort.Slice(metaList, createSortFunc(meta.KeyID, true, metaList))
		sort.Slice(metaList, createSortFunc(api.KeyID, true, metaList))
	} else if s.order == RandomOrder {
		rand.Shuffle(len(metaList), func(i, j int) {
			metaList[i], metaList[j] = metaList[j], metaList[i]
		})
	} else {
		sort.Slice(metaList, createSortFunc(s.order, s.descending, metaList))
	}

Changes to search/select.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 search provides a zettel search.
package search

import (
	"strings"

	"zettelstore.de/c/api"
	"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 }
func matchNever(string) bool  { return false }
func matchAlways(string) bool { return true }

type matchSpec struct {
	key   string
	match matchFunc
}

// compileSelect calculates a selection func based on the given select criteria.
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102
103

104
105
106
107
108
109

110
111
112
113

114
115

116
117

118
119

120
121

122
123

124
125
126

127
128
129
130
131
132
133
134

135
136
137
138
139
140
141
142

143
144
145

146
147
148
149
150
151
152
153

154
155
156
157
158
159

160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185
186
187

188
189
190
191
192
193
194
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102
103

104
105
106
107
108
109

110
111
112
113

114
115

116
117

118
119

120
121

122
123

124
125
126

127
128
129
130
131
132
133
134

135
136
137
138
139
140
141
142

143
144
145

146
147
148
149
150
151
152
153

154
155
156
157
158
159

160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185
186
187

188
189
190
191
192
193
194
195







-
+








-
+





-
+



-
+

-
+

-
+

-
+

-
+

-
+


-
+







-
+







-
+


-
+







-
+





-
+

















-
+









-
+







	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)
	return createMatchFunc(key, posValues, false), createMatchFunc(key, negValues, true)
}

// opValue is an expValue, but w/o the field "negate"
type opValue struct {
	value string
	op    compareOp
}

func createMatchFunc(key string, values []opValue) matchFunc {
func createMatchFunc(key string, values []opValue, negate bool) matchFunc {
	if len(values) == 0 {
		return nil
	}
	switch meta.Type(key) {
	case meta.TypeBool:
		return createMatchBoolFunc(values)
		return createMatchBoolFunc(values, negate)
	case meta.TypeCredential:
		return matchNever
	case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout
		return createMatchIDFunc(values)
		return createMatchIDFunc(values, negate)
	case meta.TypeIDSet:
		return createMatchIDSetFunc(values)
		return createMatchIDSetFunc(values, negate)
	case meta.TypeTagSet:
		return createMatchTagSetFunc(values)
		return createMatchTagSetFunc(values, negate)
	case meta.TypeWord:
		return createMatchWordFunc(values)
		return createMatchWordFunc(values, negate)
	case meta.TypeWordSet:
		return createMatchWordSetFunc(values)
		return createMatchWordSetFunc(values, negate)
	}
	return createMatchStringFunc(values)
	return createMatchStringFunc(values, negate)
}

func createMatchBoolFunc(values []opValue) matchFunc {
func createMatchBoolFunc(values []opValue, negate bool) 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 {
			if (bValue == v) == negate {
				return false
			}
		}
		return true
	}
}

func createMatchIDFunc(values []opValue) matchFunc {
func createMatchIDFunc(values []opValue, negate bool) matchFunc {
	return func(value string) bool {
		for _, v := range values {
			if !strings.HasPrefix(value, v.value) {
			if strings.HasPrefix(value, v.value) == negate {
				return false
			}
		}
		return true
	}
}

func createMatchIDSetFunc(values []opValue) matchFunc {
func createMatchIDSetFunc(values []opValue, negate bool) 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) {
				if matchAllID(ids, neededID.value) == 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 []opValue) matchFunc {
func createMatchTagSetFunc(values []opValue, negate bool) matchFunc {
	tagValues := processTagSet(preprocessSet(sliceToLower(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) {
				if matchAllTag(tags, neededTag.value, neededTag.equal) == negate {
					return false
				}
			}
		}
		return true
	}
}
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
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







-
+




-
+







-
+





-
+








-
+




-
+







				return true
			}
		}
	}
	return false
}

func createMatchWordFunc(values []opValue) matchFunc {
func createMatchWordFunc(values []opValue, negate bool) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if value != v.value {
			if (value == v.value) == negate {
				return false
			}
		}
		return true
	}
}

func createMatchWordSetFunc(values []opValue) matchFunc {
func createMatchWordSetFunc(values []opValue, negate bool) 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) {
				if matchAllWord(words, neededWord.value) == negate {
					return false
				}
			}
		}
		return true
	}
}

func createMatchStringFunc(values []opValue) matchFunc {
func createMatchStringFunc(values []opValue, negate bool) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if !strings.Contains(value, v.value) {
			if strings.Contains(value, v.value) == negate {
				return false
			}
		}
		return true
	}
}

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







-
+








-
-
+
+







			}
		}
		for _, s := range negSpecs {
			if s.match == nil {
				if _, ok := m.Get(s.key); ok {
					return false
				}
			} else if value, ok := getMeta(m, s.key); !ok || s.match(value) {
			} else if value, ok := getMeta(m, s.key); !ok || !s.match(value) {
				return false
			}
		}
		return true
	}
}

func getMeta(m *meta.Meta, key string) (string, bool) {
	if key == meta.KeyTags {
		return m.Get(meta.KeyAllTags)
	if key == api.KeyTags {
		return m.Get(api.KeyAllTags)
	}
	return m.Get(key)
}

func sliceToLower(sl []opValue) []opValue {
	result := make([]opValue, 0, len(sl))
	for _, s := range sl {

Changes to search/sorter.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
31
32







+







-
+








// Package search provides a zettel search.
package search

import (
	"strconv"

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

type sortFunc func(i, j int) bool

func createSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc {
	keyType := meta.Type(key)
	if key == meta.KeyID || keyType == meta.TypeCredential {
	if key == api.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
	}
	if keyType == meta.TypeBool {
		return createSortBoolFunc(ml, key, descending)

Changes to strfun/strfun.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
8
9
10
11
12
13
14
15
16

17
18
19






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

39
40

41
42
43

44
45

46
47
48
49
50
51
52
53
54







+

-



-
-
-
-
-
-



















-
+

-
+


-
+

-
+








// under this license.
//-----------------------------------------------------------------------------

// Package strfun provides some string functions.
package strfun

import (
	"bytes"
	"strings"
	"unicode"
	"unicode/utf8"
)

// TrimSpaceRight returns a slice of the string s, with all trailing white space removed,
// as defined by Unicode.
func TrimSpaceRight(s string) string {
	return strings.TrimRightFunc(s, unicode.IsSpace)
}

// Length returns the number of runes in the given string.
func Length(s string) int {
	return utf8.RuneCountInString(s)
}

// JustifyLeft ensures that the string has a defined length.
func JustifyLeft(s string, maxLen int, pad rune) string {
	if maxLen < 1 {
		return ""
	}
	runes := make([]rune, 0, len(s))
	for _, r := range s {
		runes = append(runes, r)
	}
	if len(runes) > maxLen {
		runes = runes[:maxLen]
		runes[maxLen-1] = '\u2025'
	}

	var sb strings.Builder
	var buf bytes.Buffer
	for _, r := range runes {
		sb.WriteRune(r)
		buf.WriteRune(r)
	}
	for i := 0; i < maxLen-len(runes); i++ {
		sb.WriteRune(pad)
		buf.WriteRune(pad)
	}
	return sb.String()
	return buf.String()
}

// SplitLines splits the given string into a list of lines.
func SplitLines(s string) []string {
	return strings.FieldsFunc(s, func(r rune) bool {
		return r == '\n' || r == '\r'
	})
}

Changes to strfun/strfun_test.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
13
14
15
16
17
18
19

































20
21
22
23
24
25
26







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








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"},
		{" ", ""},
		{space, ""},
		{space + "abc" + space, space + "abc"},
		{" \t\r\n \t\t\r\r\n\n ", ""},
		{" \t\r\n x\t\t\r\r\n\n ", " \t\r\n x"},
		{" \u2000\t\r\n x\t\t\r\r\ny\n \u3000", " \u2000\t\r\n x\t\t\r\r\ny"},
		{"1 \t\r\n2", "1 \t\r\n2"},
		{" x\x80", " x\x80"},
		{" x\xc0", " x\xc0"},
		{"x \xc0\xc0 ", "x \xc0\xc0"},
		{"x \xc0", "x \xc0"},
		{"x \xc0 ", "x \xc0"},
		{"x \xc0\xc0 ", "x \xc0\xc0"},
		{"x ☺\xc0\xc0 ", "x ☺\xc0\xc0"},
		{"x ☺ ", "x ☺"},
	}
	for i, tc := range testcases {
		got := strfun.TrimSpaceRight(tc.in)
		if got != tc.exp {
			t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got)
		}
	}
}

func TestLength(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  string
		exp int
	}{
		{"", 0},

Changes to template/mustache.go.

333
334
335
336
337
338
339
340

341
342
343
344
345
346
347
333
334
335
336
337
338
339

340
341
342
343
344
345
346
347







-
+







		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment
		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err := tmpl.parseSection(sn)
			err = tmpl.parseSection(sn)
			if err != nil {
				return err
			}
			section.nodes = append(section.nodes, sn)
		case '/':
			name := strings.TrimSpace(tag[1:])
			if name != section.name {
405
406
407
408
409
410
411
412

413
414
415
416
417
418
419
405
406
407
408
409
410
411

412
413
414
415
416
417
418
419







-
+







		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment
		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err := tmpl.parseSection(sn)
			err = tmpl.parseSection(sn)
			if err != nil {
				return err
			}
			tmpl.nodes = append(tmpl.nodes, sn)
		case '/':
			return parseError{tmpl.curline, "unmatched close tag"}
		case '>':
560
561
562
563
564
565
566
567

568
569
570
571
572
573
574
560
561
562
563
564
565
566

567
568
569
570
571
572
573
574







-
+







			for i := 0; i < valLen; i++ {
				enumeration[i] = val.Index(i)
			}
			topStack := len(stack)
			stack = append(stack, enumeration[0])
			for _, elem := range enumeration {
				stack[topStack] = elem
				if err := tmpl.renderNodes(w, section.nodes, stack); err != nil {
				if err = tmpl.renderNodes(w, section.nodes, stack); err != nil {
					return err
				}
			}
			return nil
		case reflect.Map, reflect.Struct:
			return tmpl.renderNodes(w, section.nodes, append(stack, value))
		}
610
611
612
613
614
615
616
617

618
619
620
621
622
623
624
610
611
612
613
614
615
616

617
618
619
620
621
622
623
624







-
+







			return err
		}
	case *partialNode:
		partial, err := getPartials(n.prov, n.name, n.indent)
		if err != nil {
			return err
		}
		if err := partial.renderTemplate(w, stack); err != nil {
		if err = partial.renderTemplate(w, stack); err != nil {
			return err
		}
	}
	return nil
}

func (tmpl *Template) renderTemplate(w io.Writer, stack []reflect.Value) error {

Changes to template/spec_test.go.

203
204
205
206
207
208
209
210
211
212



213
214
215
216
217



218
219
220
221
222
223
224
203
204
205
206
207
208
209



210
211
212
213
214



215
216
217
218
219
220
221
222
223
224







-
-
-
+
+
+


-
-
-
+
+
+







		if !ok {
			t.Errorf("Unexpected file %s, consider adding to enabledFiles", file)
			continue
		}
		if enabled == nil {
			continue
		}
		b, err := os.ReadFile(path)
		if err != nil {
			t.Fatal(err)
		b, err2 := os.ReadFile(path)
		if err2 != nil {
			t.Fatal(err2)
		}
		var suite specTestSuite
		err = json.Unmarshal(b, &suite)
		if err != nil {
			t.Fatal(err)
		err2 = json.Unmarshal(b, &suite)
		if err2 != nil {
			t.Fatal(err2)
		}
		for _, test := range suite.Tests {
			runTest(t, file, &test)
		}
	}
}

Deleted testdata/content/blockcomment/20200215204700.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
title: Simple Test

%%%
No render
%%%
%%%{-}
Render
%%%

Deleted testdata/content/cite/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

[@Stern18]{-}

Deleted testdata/content/comment/20200215204700.zettel.

1
2
3
4




-
-
-
-
title: Simple Test

% No comment
%% Comment

Deleted testdata/content/descrlist/20200226122100.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Simple Test

; Zettel
: Paper
: Note
; Zettelkasten
: Slip box

Deleted testdata/content/edit/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

~~delete~~{-}
__insert__{-}
~~kill~~{-}__create__{-}

Deleted testdata/content/embed/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

{{abc}}

Deleted testdata/content/footnote/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

Text[^foot]{=sidebar}

Deleted testdata/content/format/20200215204700.zettel.

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



















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
title: Simple Test
role: zettel

//italic//
//emph//{-}
**bold**
**strong**{-}
__unterline__
~~strike~~
''monospace''
^^superscript^^
,,subscript,,
""Quotes""
<<Quotation<<
;;small;;
::span::
``code``
++input++
==output==

Deleted testdata/content/format/20201107164400.zettel.

1
2
3
4




-
-
-
-
title: Nested Lang
role: zettel

::""abc""::{lang=fr}

Deleted testdata/content/heading/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

=== First

Deleted testdata/content/hrule/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

---

Deleted testdata/content/link/20200215204700.zettel.

1
2
3
4
5
6
7
8
9
10
11











-
-
-
-
-
-
-
-
-
-
-
title: Simple Test

[[Home|https://zettelstore.de/z]]
[[https://zettelstore.de]]
[[Config|00000000000100]]
[[00000000000100]]
[[Frag|#frag]]
[[#frag]]
[[H|/hosted]]
[[B|//based]]
[[R|../rel]]

Deleted testdata/content/list/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

* Item 1
* Item 2
* Item 3

Deleted testdata/content/list/20200217194800.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
title: Second list

* Item1.1
* Item1.2
* Item1.3

* Item2.1
* Item2.2

Deleted testdata/content/list/20200516105700.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Schachtelliste

* T1
** T2
* T3
** T4
* T5

Deleted testdata/content/literal/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

++input++
``program``
==output==

Deleted testdata/content/mark/20200215204700.zettel.

1
2
3



-
-
-
title: Simple Test

[!mark]

Deleted testdata/content/paragraph/20200215185900.zettel.

1
2
3
4




-
-
-
-
title: Simple Zettel
tags: #test #simple

This is a zettel for testing.

Deleted testdata/content/paragraph/20200217151800.zettel.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
title: Continuation after Paragraph
tags: #test #paragraph

Text Text
*abc

Text Text
* abc

Deleted testdata/content/png/20200512180900.png.

cannot compute difference between binary files

Deleted testdata/content/quoteblock/20200215204700.zettel.

1
2
3
4
5





-
-
-
-
-
title: Simple Test

<<<
To be or not to be.
<<< Romeo

Deleted testdata/content/spanblock/20200215204700.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Simple Test

:::
A simple
   span
and much more
:::

Deleted testdata/content/table/20200215204700.zettel.

1
2
3
4




-
-
-
-
title: Simple Test

|c1|c2|c3|
|d1||d3

Deleted testdata/content/table/20200618140700.zettel.

1
2
3
4
5
6






-
-
-
-
-
-
title: Testtable

|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3

Deleted testdata/content/verbatim/20200215204700.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
title: Simple Test

```
if __name__ == "main":
  print("Hello, World")
exit(0)
```

Deleted testdata/content/verseblock/20200215204700.zettel.

1
2
3
4
5
6
7
8
9
10
11











-
-
-
-
-
-
-
-
-
-
-
title: Simple Test

"""
A line
  another line
Back

Paragraph

    Spacy  Para
""" Author

Changes to testdata/meta/title/20200310110300.zettel.

1


1
-
+
title: A ""Title"" with //Markup//, ``Zettelmarkup``{=zmk}
title: A ""Title"" with __Markup__, ``Zettelmarkup``{=zmk}

Added testdata/testbox/20211019200500.zettel.










1
2
3
4
5
6
7
8
9
+
+
+
+
+
+
+
+
+
id: 20211019200500
title: Collection of all users
role: zettel
syntax: zmk

* [[owner|20210629163300]]
* [[writer|20210629165000]]
* [[reader|20210629165024]]
* [[creator|20210629165050]]

Added testdata/testbox/20211020121000.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
id: 20211020121000
title: ABC
role: zettel
syntax: zmk
modified: 20211020191331
visibility: owner

Ab C

Added testdata/testbox/20211020121100.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121100
title: 10*ABC
role: zettel
syntax: zmk

{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}

Added testdata/testbox/20211020121145.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121145
title: 10*10*ABC
role: zettel
syntax: zmk

{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}

Added testdata/testbox/20211020121300.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121300
title: 10*10*10*ABC
role: zettel
syntax: zmk

{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}

Added testdata/testbox/20211020121400.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020121400
title: 10*10*10*10*ABC
role: zettel
syntax: zmk

{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}

Added testdata/testbox/20211020182600.zettel.








1
2
3
4
5
6
7
+
+
+
+
+
+
+
id: 20211020182600
title: Self-recursive transclusion
role: zettel
syntax: zmk
modified: 20211020182712

{{20211020182600}}

Added testdata/testbox/20211020183700.zettel.








1
2
3
4
5
6
7
+
+
+
+
+
+
+
id: 20211020183700
title: Indirect Recursive 1
role: zettel
syntax: zmk
modified: 20211020183932

{{20211020183800}}

Added testdata/testbox/20211020183800.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020183800
title: Indirect Recursive 2
role: zettel
syntax: zmk

{{20211020183700}}

Added testdata/testbox/20211020184300.zettel.






1
2
3
4
5
+
+
+
+
+
id: 20211020184300
title: Empty Zettel
role: zettel
syntax: zmk

Added testdata/testbox/20211020184342.zettel.







1
2
3
4
5
6
+
+
+
+
+
+
id: 20211020184342
title: Transclude empty Zettel
role: zettel
syntax: zmk

{{20211020184300}}

Added testdata/testbox/20211020185400.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
id: 20211020185400
title: Self embed zettel
role: zettel
syntax: zmk
modified: 20211020185512

{{#mark}}
[!mark] ABC

Added tests/client/client_test.go.















































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore-client.
//
// Zettelstore client is licensed under the latest version of the EUPL
// (European Union Public License). Please see file LICENSE.txt for your rights
// and obligations under this license.
//-----------------------------------------------------------------------------

// Package client provides a client for accessing the Zettelstore via its API.
package client_test

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

	"zettelstore.de/c/api"
	"zettelstore.de/c/client"
)

func nextZid(zid api.ZettelID) api.ZettelID {
	numVal, err := strconv.Atoi(string(zid))
	if err != nil {
		panic(err)
	}
	return api.ZettelID(fmt.Sprintf("%014d", numVal+1))
}

func TestNextZid(t *testing.T) {
	testCases := []struct {
		zid, exp api.ZettelID
	}{
		{api.ZettelID("00000000000000"), api.ZettelID("00000000000001")},
	}
	for i, tc := range testCases {
		if got := nextZid(tc.zid); got != tc.exp {
			t.Errorf("%d: zid=%q, exp=%q, got=%q", i, tc.zid, tc.exp, got)
		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 46
		configRoleZettel = 27
		writerZettel     = ownerZettel - 23
		readerZettel     = ownerZettel - 23
		creatorZettel    = 10
		publicZettel     = 7
	)

	testdata := []struct {
		user string
		exp  int
	}{
		{"", publicZettel},
		{"creator", creatorZettel},
		{"reader", readerZettel},
		{"writer", writerZettel},
		{"owner", ownerZettel},
	}

	t.Parallel()
	c := getClient()
	query := url.Values{api.QueryKeyEncoding: {api.EncodingHTML}} // 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)
			q, l, err := c.ListZettelJSON(context.Background(), query)
			if err != nil {
				tt.Error(err)
				return
			}
			if q != "" {
				tt.Errorf("Query should be empty, but is %q", q)
			}
			got := len(l)
			if got != tc.exp {
				tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
			}
		})
	}
	q, l, err := c.ListZettelJSON(context.Background(), url.Values{api.KeyRole: {api.ValueRoleConfiguration}})
	if err != nil {
		t.Error(err)
		return
	}
	expQ := "role MATCH configuration"
	if q != expQ {
		t.Errorf("Query should be %q, but is %q", expQ, q)
	}
	got := len(l)
	if got != 27 {
		t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l)
	}

	pl, err := c.ListZettel(context.Background(), url.Values{api.KeyRole: {api.ValueRoleConfiguration}})
	if err != nil {
		t.Error(err)
		return
	}
	compareZettelList(t, pl, l)
}

func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaJSON) {
	t.Helper()
	if len(pl) != len(l) {
		t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(pl), len(l))
	} else {
		for i, line := range pl {
			if got := api.ZettelID(line[:14]); got != l[i].ID {
				t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got)
			}
		}
	}
}

func TestGetZettelJSON(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if m := z.Meta; len(m) == 0 {
		t.Errorf("Exptected non-empty meta, but got %v", z.Meta)
	}
	if z.Content == "" || z.Encoding != "" {
		t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding)
	}

	m, err := c.GetMeta(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if len(m) != len(z.Meta) {
		t.Errorf("Pure meta differs from zettel meta: %s vs %s", m, z.Meta)
		return
	}
	for k, v := range z.Meta {
		got, ok := m[k]
		if !ok {
			t.Errorf("Pure meta has no key %q", k)
			continue
		}
		if got != v {
			t.Errorf("Pure meta has different value for key %q: %q vs %q", k, got, v)
		}
	}
}

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

func checkZid(t *testing.T, expected, got api.ZettelID) bool {
	t.Helper()
	if expected != got {
		t.Errorf("Expected a Zid %q, but got %q", expected, got)
		return false
	}
	return true
}

func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected api.ZettelID) {
	t.Helper()
	if got := api.ZettelID(l[pos].ID); got != expected {
		t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got)
	}
}

func TestGetZettelOrder(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.GetZettelOrder(context.Background(), api.ZidTOCNewTemplate)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, api.ZidTOCNewTemplate, rl.ID) {
		return
	}
	l := rl.List
	if got := len(l); got != 2 {
		t.Errorf("Expected list of length 2, got %d", got)
		return
	}
	checkListZid(t, l, 0, api.ZidTemplateNewZettel)
	checkListZid(t, l, 1, api.ZidTemplateNewUser)
}

func TestGetZettelContext(t *testing.T) {
	const (
		allUserZid = api.ZettelID("20211019200500")
		ownerZid   = api.ZettelID("20210629163300")
		writerZid  = api.ZettelID("20210629165000")
		readerZid  = api.ZettelID("20210629165024")
		creatorZid = api.ZettelID("20210629165050")
		limitAll   = 3
	)
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, ownerZid, rl.ID) {
		return
	}
	l := rl.List
	if got := len(l); got != limitAll {
		t.Errorf("Expected list of length %d, got %d", limitAll, got)
		t.Error(rl)
		return
	}
	checkListZid(t, l, 0, allUserZid)
	checkListZid(t, l, 1, writerZid)
	checkListZid(t, l, 2, readerZid)
	// checkListZid(t, l, 3, creatorZid)

	rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, ownerZid, rl.ID) {
		return
	}
	l = rl.List
	if got := len(l); got != 1 {
		t.Errorf("Expected list of length 1, got %d", got)
		return
	}
	checkListZid(t, l, 0, allUserZid)
}

func TestGetZettelLinks(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	zl, err := c.GetZettelLinks(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, api.ZidDefaultHome, zl.ID) {
		return
	}
	if got := len(zl.Linked.Outgoing); got != 4 {
		t.Errorf("Expected 4 outgoing links, got %d", got)
	}
	if got := len(zl.Linked.Local); got != 1 {
		t.Errorf("Expected 1 local link, got %d", got)
	}
	if got := len(zl.Linked.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()
	}
}

Added tests/client/crud_test.go.
































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore-client.
//
// Zettelstore client is licensed under the latest version of the EUPL
// (European Union Public License). Please see file LICENSE.txt for your rights
// and obligations under this license.
//-----------------------------------------------------------------------------

package client_test

import (
	"context"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/c/client"
)

// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.

func TestCreateGetRenameDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	zettel := `title: A Test

Example content.`
	c := getClient()
	c.SetAuth("owner", "owner")
	zid, err := c.CreateZettel(context.Background(), []byte(zettel))
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	if !zid.IsValid() {
		t.Error("Invalid zettel ID", zid)
		return
	}
	data, err := c.GetZettel(context.Background(), zid, api.PartZettel)
	if err != nil {
		t.Error("Cannot read zettel", zid, err)
		return
	}
	exp := `title: A Test
role: zettel
syntax: zmk

Example content.`
	if string(data) != exp {
		t.Errorf("Expected zettel data: %q, but got %q", exp, data)
	}
	newZid := nextZid(zid)
	err = c.RenameZettel(context.Background(), zid, newZid)
	if err != nil {
		t.Error("Cannot rename", zid, ":", err)
		newZid = zid
	}

	doDelete(t, c, newZid)
}

func TestCreateGetRenameDeleteZettelJSON(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("creator", "creator")
	zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{
		Meta:     nil,
		Encoding: "",
		Content:  "Example",
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	if !zid.IsValid() {
		t.Error("Invalid zettel ID", zid)
		return
	}
	newZid := nextZid(zid)
	c.SetAuth("owner", "owner")
	err = c.RenameZettel(context.Background(), zid, newZid)
	if err != nil {
		t.Error("Cannot rename", zid, ":", err)
		newZid = zid
	}

	c.SetAuth("owner", "owner")
	doDelete(t, c, newZid)
}

func TestCreateGetDeleteZettelJSON(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("owner", "owner")
	zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{
		Meta: api.ZettelMeta{
			api.KeyTitle: "A\nTitle", // \n must be converted into a space
		},
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	z, err := c.GetZettelJSON(context.Background(), zid)
	if err != nil {
		t.Error("Cannot get zettel:", zid, err)
	} else {
		exp := "A Title"
		if got := z.Meta[api.KeyTitle]; got != exp {
			t.Errorf("Expected title %q, but got %q", exp, got)
		}
	}
	doDelete(t, c, zid)
}

func TestUpdateZettel(t *testing.T) {
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel)
	if err != nil {
		t.Error(err)
		return
	}
	if !strings.HasPrefix(string(z), "title: Home\n") {
		t.Error("Got unexpected zettel", z)
		return
	}
	newZettel := `title: Empty Home
role: zettel
syntax: zmk

Empty`
	err = c.UpdateZettel(context.Background(), api.ZidDefaultHome, []byte(newZettel))
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel)
	if err != nil {
		t.Error(err)
		return
	}
	if string(zt) != newZettel {
		t.Errorf("Expected zettel %q, got %q", newZettel, zt)
	}
	// Must delete to clean up for next tests
	doDelete(t, c, api.ZidDefaultHome)
}

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

	// Must delete to clean up for next tests
	c.SetAuth("owner", "owner")
	doDelete(t, c, api.ZidDefaultHome)
}

func doDelete(t *testing.T, c *client.Client, zid api.ZettelID) {
	err := c.DeleteZettel(context.Background(), zid)
	if err != nil {
		t.Helper()
		t.Error("Cannot delete", zid, ":", err)
	}
}

Added tests/client/embed_test.go.




















































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore-client.
//
// Zettelstore client is licensed under the latest version of the EUPL
// (European Union Public License). Please see file LICENSE.txt for your rights
// and obligations under this license.
//-----------------------------------------------------------------------------

package client_test

import (
	"context"
	"strings"
	"testing"

	"zettelstore.de/c/api"
)

const (
	abcZid   = api.ZettelID("20211020121000")
	abc10Zid = api.ZettelID("20211020121100")
)

func TestZettelTransclusion(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const abc10000Zid = api.ZettelID("20211020121400")
	contentMap := map[api.ZettelID]int{
		abcZid:                         1,
		abc10Zid:                       10,
		api.ZettelID("20211020121145"): 100,
		api.ZettelID("20211020121300"): 1000,
	}
	content, err := c.GetZettel(context.Background(), abcZid, api.PartContent)
	if err != nil {
		t.Error(err)
		return
	}
	baseContent := string(content)
	for zid, siz := range contentMap {
		content, err = c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML)
		if err != nil {
			t.Error(err)
			continue
		}
		sContent := string(content)
		prefix := "<p>"
		if !strings.HasPrefix(sContent, prefix) {
			t.Errorf("Content of zettel %q does not start with %q: %q", zid, prefix, stringHead(sContent))
			continue
		}
		suffix := "</p>"
		if !strings.HasSuffix(sContent, suffix) {
			t.Errorf("Content of zettel %q does not end with %q: %q", zid, suffix, stringTail(sContent))
			continue
		}
		got := sContent[len(prefix) : len(content)-len(suffix)]
		if expect := strings.Repeat(baseContent, siz); expect != got {
			t.Errorf("Unexpected content for zettel %q\nExpect: %q\nGot:    %q", zid, expect, got)
		}
	}

	content, err = c.GetEvaluatedZettel(context.Background(), abc10000Zid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	checkContentContains(t, abc10000Zid, string(content), "Too many transclusions")
}

func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("reader", "reader")

	zettelData, err := c.GetZettelJSON(context.Background(), api.ZidEmoji)
	if err != nil {
		t.Error(err)
		return
	}
	expectedEnc := "base64"
	if got := zettelData.Encoding; expectedEnc != got {
		t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got)
	}

	content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	expectedContent := "<img src=\"data:image/gif;" + expectedEnc + "," + zettelData.Content
	checkContentContains(t, abc10Zid, string(content), expectedContent)
}

func stringHead(s string) string {
	const maxLen = 40
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen-3] + "..."
}

func stringTail(s string) string {
	const maxLen = 40
	if len(s) <= maxLen {
		return s
	}
	return "..." + s[len(s)-maxLen-3:]
}

func TestRecursiveTransclusion(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const (
		selfRecursiveZid      = api.ZettelID("20211020182600")
		indirectRecursive1Zid = api.ZettelID("20211020183700")
		indirectRecursive2Zid = api.ZettelID("20211020183800")
	)
	recursiveZettel := map[api.ZettelID]api.ZettelID{
		selfRecursiveZid:      selfRecursiveZid,
		indirectRecursive1Zid: indirectRecursive2Zid,
		indirectRecursive2Zid: indirectRecursive1Zid,
	}
	for zid, errZid := range recursiveZettel {
		content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML)
		if err != nil {
			t.Error(err)
			continue
		}
		sContent := string(content)
		checkContentContains(t, zid, sContent, "Recursive transclusion")
		checkContentContains(t, zid, sContent, string(errZid))
	}
}
func TestNothingToTransclude(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const (
		transZid = api.ZettelID("20211020184342")
		emptyZid = api.ZettelID("20211020184300")
	)
	content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	sContent := string(content)
	checkContentContains(t, transZid, sContent, "<!-- Nothing to transclude")
	checkContentContains(t, transZid, sContent, string(emptyZid))
}

func TestSelfEmbedRef(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const selfEmbedZid = api.ZettelID("20211020185400")
	content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	checkContentContains(t, selfEmbedZid, string(content), "Self embed reference")
}

func checkContentContains(t *testing.T, zid api.ZettelID, content, expected string) {
	if !strings.Contains(content, expected) {
		t.Helper()
		t.Errorf("Zettel %q should contain %q, but does not: %q", zid, expected, content)
	}
}

Changes to tests/markdown_test.go.

8
9
10
11
12
13
14

15
16
17
18
19
20
21
22

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


19
20

21
22
23
24
25
26
27
28







+



-
-


-
+







// under this license.
//-----------------------------------------------------------------------------

// Package tests provides some higher-level tests.
package tests

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strings"
	"testing"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/djsonenc"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/nativeenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
37
38
39
40
41
42
43




























44
45
46
47
48
49
50







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







	HTML      string `json:"html"`
	Example   int    `json:"example"`
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	Section   string `json:"section"`
}

// exceptions lists all CommonMark tests that should not be tested for identical HTML output
var exceptions = []string{
	" - foo\n   - bar\n\t - baz\n", // 9
	"<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n", // 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
	"<http://example.com?find=\\*>\n",                          // 581
	"<http://foo.bar.baz/test?q=hello&id=22&boolean>\n",        // 594
}

var reHeadingID = regexp.MustCompile(` id="[^"]*"`)

func TestEncoderAvailability(t *testing.T) {
	t.Parallel()
	encoderMissing := false
	for _, enc := range encodings {
		enc := encoder.Create(enc, nil)
		if enc == nil {
			t.Errorf("No encoder for %q found", enc)
91
92
93
94
95
96
97
98
99
100
101

102
103

104
105
106
107
108
109
110
111
112
113

114
115
116
117
118


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

151
152

153
154


155
156
157
158
159
160




161
162
163
164
165
166
167
168
169
170




171
172
173
174
175
176
177

178
62
63
64
65
66
67
68




69
70

71
72



73
74
75
76
77

78
79
80
81


82
83
84
85
86
87

























88
89

90
91
92
93


94
95

96
97



98
99
100
101

102
103
104
105
106
107



108
109
110
111

112
113
114
115
116

117








-
-
-
-
+

-
+

-
-
-





-
+



-
-
+
+




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


-
+


+
-
-
+
+
-


-
-
-
+
+
+
+
-






-
-
-
+
+
+
+
-





-
+
-
	if err != nil {
		panic(err)
	}
	var testcases []markdownTestCase
	if err = json.Unmarshal(content, &testcases); err != nil {
		panic(err)
	}
	excMap := make(map[string]bool, len(exceptions))
	for _, exc := range exceptions {
		excMap[exc] = true
	}

	for _, tc := range testcases {
		ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown")
		ast := parser.ParseBlocks(input.NewInput([]byte(tc.Markdown)), nil, "markdown")
		testAllEncodings(t, tc, ast)
		if _, found := excMap[tc.Markdown]; !found {
			testHTMLEncoding(t, tc, ast)
		}
		testZmkEncoding(t, tc, ast)
	}
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockListNode) {
	var sb strings.Builder
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc, nil).WriteBlocks(&sb, ast)
			sb.Reset()
			encoder.Create(enc, nil).WriteBlocks(&buf, ast)
			buf.Reset()
		})
	}
}

func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockListNode) {
	htmlEncoder := encoder.Create(api.EncoderHTML, &encoder.Environment{Xhtml: true})
	var sb strings.Builder
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) {
		htmlEncoder.WriteBlocks(&sb, ast)
		gotHTML := sb.String()
		sb.Reset()

		mdHTML := tc.HTML
		mdHTML = strings.ReplaceAll(mdHTML, "\"MAILTO:", "\"mailto:")
		gotHTML = strings.ReplaceAll(gotHTML, " class=\"zs-external\"", "")
		gotHTML = strings.ReplaceAll(gotHTML, "%2A", "*") // url.QueryEscape
		if strings.Count(gotHTML, "<h") > 0 {
			gotHTML = reHeadingID.ReplaceAllString(gotHTML, "")
		}
		if gotHTML != mdHTML {
			mdHTML = strings.ReplaceAll(mdHTML, "<li>\n", "<li>")
			if gotHTML != mdHTML {
				st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML)
			}
		}
	})
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockListNode) {
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var sb strings.Builder
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		zmkEncoder.WriteBlocks(&sb, ast)
		gotFirst := sb.String()
		zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()
		sb.Reset()

		testID = tc.Example*100 + 2
		secondAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk")
		zmkEncoder.WriteBlocks(&sb, secondAst)
		gotSecond := sb.String()
		secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, secondAst)
		gotSecond := buf.String()
		sb.Reset()

		// if gotFirst != gotSecond {
		// 	st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond)
		// }

		testID = tc.Example*100 + 3
		thirdAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk")
		zmkEncoder.WriteBlocks(&sb, thirdAst)
		gotThird := sb.String()
		thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, thirdAst)
		gotThird := buf.String()
		sb.Reset()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
		}
	})

}
}

Changes to tests/regression_test.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
8
9
10
11
12
13
14
15
16
17
18
19
20
21

22
23

24
25
26
27

28
29
30
31
32
33
34







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



54
55
56
57



58
59
60
61
62
63
64
65
66
67







+






-


-
+



-







-
-
-
-
-
-
-



















-
-
-
+
+
+

-
-
-
+
+
+







// under this license.
//-----------------------------------------------------------------------------

// Package tests provides some higher-level tests.
package tests

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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/box/dirbox"
	_ "zettelstore.de/z/encoder/djsonenc"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "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"
)

var encodings = []api.EncodingEnum{
	api.EncoderHTML,
	api.EncoderDJSON,
	api.EncoderNative,
	api.EncoderText,
}

func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) {
	root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind))
	entries, err := os.ReadDir(root)
	if err != nil {
		panic(err)
	}

	cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil}
	for _, entry := range entries {
		if entry.IsDir() {
			u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple)
			if err != nil {
				panic(err)
			u, err2 := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple)
			if err2 != nil {
				panic(err2)
			}
			box, err := manager.Connect(u, &noAuth{}, &cdata)
			if err != nil {
				panic(err)
			box, err2 := manager.Connect(u, &noAuth{}, &cdata)
			if err2 != nil {
				panic(err2)
			}
			boxes = append(boxes, box)
		}
	}
	return root, boxes
}

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
101
102
103
104
105
106
107






























108
109
110
111
112
113
114
115











































116
117
118
119



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138



139
140
141
142
143
144
145
146
147
148
149
150

151
152
153
154
155
156
157
158
159


160
161
162
163
164
165
166
167
168







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








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




-
-
-
+
+
+
















-
-
-
+
+
+









-
+








-
-
+
+







	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, enc api.EncodingEnum) {
	t.Helper()
	var env encoder.Environment
	if enc := encoder.Create(enc, &env); enc != nil {
		var sb strings.Builder
		enc.WriteBlocks(&sb, zn.Ast)
		checkFileContent(t, resultName, sb.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}

func checkZmkEncoder(t *testing.T, zn *ast.ZettelNode) {
	zmkEncoder := encoder.Create(api.EncoderZmk, 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 {
	u, err := url.Parse(p.Location())
	if err != nil {
		panic("Unable to parse URL '" + p.Location() + "': " + err.Error())
	}
	return u.Path[len(root):]
}

func checkContentBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList := []*meta.Meta{}
	err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) })
	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 _, enc := range encodings {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "content", boxName, z.Zid.String()+"."+enc.String())
				checkBlocksFile(st, resultName, z, enc)
			})
		}
		t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) {
			checkZmkEncoder(st, z)
		})
	}
	if err := ss.Stop(context.Background()); err != nil {
		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))
	}
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
	t.Helper()

	if enc := encoder.Create(enc, nil); enc != nil {
		var sb strings.Builder
		enc.WriteMeta(&sb, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, sb.String())
		var buf bytes.Buffer
		enc.WriteMeta(&buf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, buf.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}

func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList := []*meta.Meta{}
	err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) })
	if err != nil {
		panic(err)
	}
	for _, meta := range metaList {
		zettel, err := p.GetZettel(context.Background(), meta.Zid)
		if err != nil {
			panic(err)
		zettel, err2 := p.GetZettel(context.Background(), meta.Zid)
		if err2 != nil {
			panic(err2)
		}
		z := parser.ParseZettel(zettel, "", testConfig)
		for _, enc := range encodings {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String())
				checkMetaFile(st, resultName, z, enc)
			})
		}
	}
	if err := ss.Stop(context.Background()); err != nil {
	if err = ss.Stop(context.Background()); err != nil {
		panic(err)
	}
}

type myConfig struct{}

func (*myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m }
func (*myConfig) GetDefaultTitle() string                  { return "" }
func (*myConfig) GetDefaultRole() string                   { return meta.ValueRoleZettel }
func (*myConfig) GetDefaultSyntax() string                 { return meta.ValueSyntaxZmk }
func (*myConfig) GetDefaultRole() string                   { return api.ValueRoleZettel }
func (*myConfig) GetDefaultSyntax() string                 { return api.ValueSyntaxZmk }
func (*myConfig) GetDefaultLang() string                   { return "" }
func (*myConfig) GetDefaultVisibility() meta.Visibility    { return meta.VisibilityPublic }
func (*myConfig) GetFooterHTML() string                    { return "" }
func (*myConfig) GetHomeZettel() id.Zid                    { return id.Invalid }
func (*myConfig) GetListPageSize() int                     { return 0 }
func (*myConfig) GetMarkerExternal() string                { return "" }
func (*myConfig) GetSiteName() string                      { return "" }

Deleted tests/result/content/blockcomment/20200215204700.djson.

1

-
[{"t":"CommentBlock","l":["No render"]},{"t":"CommentBlock","a":{"-":""},"l":["Render"]}]

Deleted tests/result/content/blockcomment/20200215204700.html.

1
2
3



-
-
-
<!--
Render
-->

Deleted tests/result/content/blockcomment/20200215204700.native.

1
2


-
-
[CommentBlock "No render"],
[CommentBlock ("",[-]) "Render"]

Deleted tests/result/content/blockcomment/20200215204700.text.

Deleted tests/result/content/cite/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Cite","a":{"-":""},"s":"Stern18"}]}]

Deleted tests/result/content/cite/20200215204700.html.

1

-
<p>Stern18</p>

Deleted tests/result/content/cite/20200215204700.native.

1

-
[Para Cite ("",[-]) "Stern18"]

Deleted tests/result/content/cite/20200215204700.text.

Deleted tests/result/content/comment/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"%"},{"t":"Space"},{"t":"Text","s":"No"},{"t":"Space"},{"t":"Text","s":"comment"},{"t":"Soft"},{"t":"Comment","s":"Comment"}]}]

Deleted tests/result/content/comment/20200215204700.html.

1
2


-
-
<p>% No comment
<!-- Comment --></p>

Deleted tests/result/content/comment/20200215204700.native.

1

-
[Para Text "%",Space,Text "No",Space,Text "comment",Space,Comment "Comment"]

Deleted tests/result/content/comment/20200215204700.text.

1

-
% No comment 

Deleted tests/result/content/descrlist/20200226122100.djson.

1

-
[{"t":"DescriptionList","g":[[[{"t":"Text","s":"Zettel"}],[{"t":"Para","i":[{"t":"Text","s":"Paper"}]}],[{"t":"Para","i":[{"t":"Text","s":"Note"}]}]],[[{"t":"Text","s":"Zettelkasten"}],[{"t":"Para","i":[{"t":"Text","s":"Slip"},{"t":"Space"},{"t":"Text","s":"box"}]}]]]}]

Deleted tests/result/content/descrlist/20200226122100.html.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
<dl>
<dt>Zettel</dt>
<dd>Paper</dd>
<dd>Note</dd>
<dt>Zettelkasten</dt>
<dd>Slip box</dd>
</dl>

Deleted tests/result/content/descrlist/20200226122100.native.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
[DescriptionList
 [Term [Text "Zettel"],
  [Description
   [Para Text "Paper"]],
  [Description
   [Para Text "Note"]]],
 [Term [Text "Zettelkasten"],
  [Description
   [Para Text "Slip",Space,Text "box"]]]]

Deleted tests/result/content/descrlist/20200226122100.text.

1
2
3
4
5





-
-
-
-
-
Zettel
Paper
Note
Zettelkasten
Slip box

Deleted tests/result/content/edit/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Delete","i":[{"t":"Text","s":"delete"}]},{"t":"Soft"},{"t":"Insert","i":[{"t":"Text","s":"insert"}]},{"t":"Soft"},{"t":"Delete","i":[{"t":"Text","s":"kill"}]},{"t":"Insert","i":[{"t":"Text","s":"create"}]}]}]

Deleted tests/result/content/edit/20200215204700.html.

1
2
3



-
-
-
<p><del>delete</del>
<ins>insert</ins>
<del>kill</del><ins>create</ins></p>

Deleted tests/result/content/edit/20200215204700.native.

1

-
[Para Delete [Text "delete"],Space,Insert [Text "insert"],Space,Delete [Text "kill"],Insert [Text "create"]]

Deleted tests/result/content/edit/20200215204700.text.

1

-
delete insert killcreate

Deleted tests/result/content/embed/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Embed","s":"abc"}]}]

Deleted tests/result/content/embed/20200215204700.html.

1

-
<p><img src="abc" alt=""></p>

Deleted tests/result/content/embed/20200215204700.native.

1

-
[Para Embed EXTERNAL "abc"]

Deleted tests/result/content/embed/20200215204700.text.

Deleted tests/result/content/footnote/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Footnote","a":{"":"sidebar"},"i":[{"t":"Text","s":"foot"}]}]}]

Deleted tests/result/content/footnote/20200215204700.html.

1
2
3
4




-
-
-
-
<p>Text<sup id="fnref:1"><a href="#fn:1" class="zs-footnote-ref" role="doc-noteref">1</a></sup></p>
<ol class="zs-endnotes">
<li id="fn:1" role="doc-endnote">foot <a href="#fnref:1" class="zs-footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></li>
</ol>

Deleted tests/result/content/footnote/20200215204700.native.

1

-
[Para Text "Text",Footnote ("sidebar",[]) [Text "foot"]]

Deleted tests/result/content/footnote/20200215204700.text.

1

-
Text foot

Deleted tests/result/content/format/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Italic","i":[{"t":"Text","s":"italic"}]},{"t":"Soft"},{"t":"Emph","i":[{"t":"Text","s":"emph"}]},{"t":"Soft"},{"t":"Bold","i":[{"t":"Text","s":"bold"}]},{"t":"Soft"},{"t":"Strong","i":[{"t":"Text","s":"strong"}]},{"t":"Soft"},{"t":"Underline","i":[{"t":"Text","s":"unterline"}]},{"t":"Soft"},{"t":"Strikethrough","i":[{"t":"Text","s":"strike"}]},{"t":"Soft"},{"t":"Mono","i":[{"t":"Text","s":"monospace"}]},{"t":"Soft"},{"t":"Super","i":[{"t":"Text","s":"superscript"}]},{"t":"Soft"},{"t":"Sub","i":[{"t":"Text","s":"subscript"}]},{"t":"Soft"},{"t":"Quote","i":[{"t":"Text","s":"Quotes"}]},{"t":"Soft"},{"t":"Quotation","i":[{"t":"Text","s":"Quotation"}]},{"t":"Soft"},{"t":"Small","i":[{"t":"Text","s":"small"}]},{"t":"Soft"},{"t":"Span","i":[{"t":"Text","s":"span"}]},{"t":"Soft"},{"t":"Code","s":"code"},{"t":"Soft"},{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Output","s":"output"}]}]

Deleted tests/result/content/format/20200215204700.html.

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
















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<p><i>italic</i>
<em>emph</em>
<b>bold</b>
<strong>strong</strong>
<u>unterline</u>
<s>strike</s>
<span style="font-family:monospace">monospace</span>
<sup>superscript</sup>
<sub>subscript</sub>
"Quotes"
<q>Quotation</q>
<small>small</small>
<span>span</span>
<code>code</code>
<kbd>input</kbd>
<samp>output</samp></p>

Deleted tests/result/content/format/20200215204700.native.

1

-
[Para Italic [Text "italic"],Space,Emph [Text "emph"],Space,Bold [Text "bold"],Space,Strong [Text "strong"],Space,Underline [Text "unterline"],Space,Strikethrough [Text "strike"],Space,Mono [Text "monospace"],Space,Super [Text "superscript"],Space,Sub [Text "subscript"],Space,Quote [Text "Quotes"],Space,Quotation [Text "Quotation"],Space,Small [Text "small"],Space,Span [Text "span"],Space,Code "code",Space,Input "input",Space,Output "output"]

Deleted tests/result/content/format/20200215204700.text.

1

-
italic emph bold strong unterline strike monospace superscript subscript Quotes Quotation small span code input output

Deleted tests/result/content/format/20201107164400.djson.

1

-
[{"t":"Para","i":[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]}]

Deleted tests/result/content/format/20201107164400.html.

1

-
<p><span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span></p>

Deleted tests/result/content/format/20201107164400.native.

1

-
[Para Span ("",[lang="fr"]) [Quote [Text "abc"]]]

Deleted tests/result/content/format/20201107164400.text.

1

-
abc

Deleted tests/result/content/heading/20200215204700.djson.

1

-
[{"t":"Heading","n":2,"s":"first","i":[{"t":"Text","s":"First"}]}]

Deleted tests/result/content/heading/20200215204700.html.

1

-
<h2 id="first">First</h2>

Deleted tests/result/content/heading/20200215204700.native.

1

-
[Heading 2 #first Text "First"]

Deleted tests/result/content/heading/20200215204700.text.

1

-
First

Deleted tests/result/content/hrule/20200215204700.djson.

1

-
[{"t":"Hrule"}]

Deleted tests/result/content/hrule/20200215204700.html.

1

-
<hr>

Deleted tests/result/content/hrule/20200215204700.native.

1

-
[Hrule]

Deleted tests/result/content/hrule/20200215204700.text.

Deleted tests/result/content/link/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Link","q":"external","s":"https://zettelstore.de/z","i":[{"t":"Text","s":"Home"}]},{"t":"Soft"},{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"Frag"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]},{"t":"Soft"},{"t":"Link","q":"based","s":"/based","i":[{"t":"Text","s":"B"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"../rel","i":[{"t":"Text","s":"R"}]}]}]

Deleted tests/result/content/link/20200215204700.html.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
<p><a href="https://zettelstore.de/z" class="zs-external">Home</a>
<a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a>
<a href="00000000000100">Config</a>
<a href="00000000000100">00000000000100</a>
<a href="#frag">Frag</a>
<a href="#frag">#frag</a>
<a href="/hosted">H</a>
<a href="/based">B</a>
<a href="../rel">R</a></p>

Deleted tests/result/content/link/20200215204700.native.

1

-
[Para Link EXTERNAL "https://zettelstore.de/z" [Text "Home"],Space,Link EXTERNAL "https://zettelstore.de" [],Space,Link ZETTEL "00000000000100" [Text "Config"],Space,Link ZETTEL "00000000000100" [],Space,Link SELF "#frag" [Text "Frag"],Space,Link SELF "#frag" [],Space,Link LOCAL "/hosted" [Text "H"],Space,Link BASED "/based" [Text "B"],Space,Link LOCAL "../rel" [Text "R"]]

Deleted tests/result/content/link/20200215204700.text.

1

-
Home  Config  Frag  H B R

Deleted tests/result/content/list/20200215204700.djson.

1

-
[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"3"}]}]]}]

Deleted tests/result/content/list/20200215204700.html.

1
2
3
4
5





-
-
-
-
-
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>

Deleted tests/result/content/list/20200215204700.native.

1
2
3
4




-
-
-
-
[BulletList
 [[Para Text "Item",Space,Text "1"]],
 [[Para Text "Item",Space,Text "2"]],
 [[Para Text "Item",Space,Text "3"]]]

Deleted tests/result/content/list/20200215204700.text.

1
2
3



-
-
-
Item 1
Item 2
Item 3

Deleted tests/result/content/list/20200217194800.djson.

1

-
[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item1.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.3"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.2"}]}]]}]

Deleted tests/result/content/list/20200217194800.html.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
<ul>
<li>Item1.1</li>
<li>Item1.2</li>
<li>Item1.3</li>
<li>Item2.1</li>
<li>Item2.2</li>
</ul>

Deleted tests/result/content/list/20200217194800.native.

1
2
3
4
5
6






-
-
-
-
-
-
[BulletList
 [[Para Text "Item1.1"]],
 [[Para Text "Item1.2"]],
 [[Para Text "Item1.3"]],
 [[Para Text "Item2.1"]],
 [[Para Text "Item2.2"]]]

Deleted tests/result/content/list/20200217194800.text.

1
2
3
4
5





-
-
-
-
-
Item1.1
Item1.2
Item1.3
Item2.1
Item2.2

Deleted tests/result/content/list/20200516105700.djson.

1

-
[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T1"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T2"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T3"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T4"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T5"}]}]]}]

Deleted tests/result/content/list/20200516105700.html.

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














-
-
-
-
-
-
-
-
-
-
-
-
-
-
<ul>
<li><p>T1</p>
<ul>
<li>T2</li>
</ul>
</li>
<li><p>T3</p>
<ul>
<li>T4</li>
</ul>
</li>
<li><p>T5</p>
</li>
</ul>

Deleted tests/result/content/list/20200516105700.native.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
[BulletList
 [[Para Text "T1"],
  [BulletList
   [[Para Text "T2"]]]],
 [[Para Text "T3"],
  [BulletList
   [[Para Text "T4"]]]],
 [[Para Text "T5"]]]

Deleted tests/result/content/list/20200516105700.text.

1
2
3
4
5





-
-
-
-
-
T1
T2
T3
T4
T5

Deleted tests/result/content/literal/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Code","s":"program"},{"t":"Soft"},{"t":"Output","s":"output"}]}]

Deleted tests/result/content/literal/20200215204700.html.

1
2
3



-
-
-
<p><kbd>input</kbd>
<code>program</code>
<samp>output</samp></p>

Deleted tests/result/content/literal/20200215204700.native.

1

-
[Para Input "input",Space,Code "program",Space,Output "output"]

Deleted tests/result/content/literal/20200215204700.text.

1

-
input program output

Deleted tests/result/content/mark/20200215204700.djson.

1

-
[{"t":"Para","i":[{"t":"Mark","s":"mark","q":"mark"}]}]

Deleted tests/result/content/mark/20200215204700.html.

1

-
<p><a id="mark"></a></p>

Deleted tests/result/content/mark/20200215204700.native.

1

-
[Para Mark "mark" #mark]

Deleted tests/result/content/mark/20200215204700.text.

Deleted tests/result/content/paragraph/20200215185900.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"This"},{"t":"Space"},{"t":"Text","s":"is"},{"t":"Space"},{"t":"Text","s":"a"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"for"},{"t":"Space"},{"t":"Text","s":"testing."}]}]

Deleted tests/result/content/paragraph/20200215185900.html.

1

-
<p>This is a zettel for testing.</p>

Deleted tests/result/content/paragraph/20200215185900.native.

1

-
[Para Text "This",Space,Text "is",Space,Text "a",Space,Text "zettel",Space,Text "for",Space,Text "testing."]

Deleted tests/result/content/paragraph/20200215185900.text.

1

-
This is a zettel for testing.

Deleted tests/result/content/paragraph/20200217151800.djson.

1

-
[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"},{"t":"Soft"},{"t":"Text","s":"*abc"}]},{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"abc"}]}]]}]

Deleted tests/result/content/paragraph/20200217151800.html.

1
2
3
4
5
6






-
-
-
-
-
-
<p>Text Text
*abc</p>
<p>Text Text</p>
<ul>
<li>abc</li>
</ul>

Deleted tests/result/content/paragraph/20200217151800.native.

1
2
3
4




-
-
-
-
[Para Text "Text",Space,Text "Text",Space,Text "*abc"],
[Para Text "Text",Space,Text "Text"],
[BulletList
 [[Para Text "abc"]]]

Deleted tests/result/content/paragraph/20200217151800.text.

1
2
3



-
-
-
Text Text *abc
Text Text
abc

Deleted tests/result/content/png/20200512180900.djson.

1

-
[{"t":"Blob","q":"20200512180900","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]

Deleted tests/result/content/png/20200512180900.html.

1

-
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="20200512180900">

Deleted tests/result/content/png/20200512180900.native.

1

-
[BLOB "20200512180900" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="]

Deleted tests/result/content/png/20200512180900.text.

Deleted tests/result/content/quoteblock/20200215204700.djson.

1

-
[{"t":"QuoteBlock","b":[{"t":"Para","i":[{"t":"Text","s":"To"},{"t":"Space"},{"t":"Text","s":"be"},{"t":"Space"},{"t":"Text","s":"or"},{"t":"Space"},{"t":"Text","s":"not"},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"be."}]}],"i":[{"t":"Text","s":"Romeo"}]}]

Deleted tests/result/content/quoteblock/20200215204700.html.

1
2
3
4




-
-
-
-
<blockquote>
<p>To be or not to be.</p>
<cite>Romeo</cite>
</blockquote>

Deleted tests/result/content/quoteblock/20200215204700.native.

1
2
3



-
-
-
[QuoteBlock
 [[Para Text "To",Space,Text "be",Space,Text "or",Space,Text "not",Space,Text "to",Space,Text "be."]],
 [Cite Text "Romeo"]]

Deleted tests/result/content/quoteblock/20200215204700.text.

1
2


-
-
To be or not to be.
Romeo

Deleted tests/result/content/spanblock/20200215204700.djson.

1

-
[{"t":"SpanBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Text","s":"simple"},{"t":"Soft"},{"t":"Space","n":3},{"t":"Text","s":"span"},{"t":"Soft"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"much"},{"t":"Space"},{"t":"Text","s":"more"}]}]}]

Deleted tests/result/content/spanblock/20200215204700.html.

1
2
3
4
5





-
-
-
-
-
<div>
<p>A simple
 span
and much more</p>
</div>

Deleted tests/result/content/spanblock/20200215204700.native.

1
2


-
-
[SpanBlock
 [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]]

Deleted tests/result/content/spanblock/20200215204700.text.

1

-
A simple  span and much more

Deleted tests/result/content/table/20200215204700.djson.

1

-
[{"t":"Table","p":[[],[[["",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],["",[{"t":"Text","s":"c3"}]]],[["",[{"t":"Text","s":"d1"}]],["",[]],["",[{"t":"Text","s":"d3"}]]]]]}]

Deleted tests/result/content/table/20200215204700.html.

1
2
3
4
5
6






-
-
-
-
-
-
<table>
<tbody>
<tr><td>c1</td><td>c2</td><td>c3</td></tr>
<tr><td>d1</td><td></td><td>d3</td></tr>
</tbody>
</table>

Deleted tests/result/content/table/20200215204700.native.

1
2
3



-
-
-
[Table
 [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]],
 [Row [Cell Default Text "d1"],[Cell Default],[Cell Default Text "d3"]]]

Deleted tests/result/content/table/20200215204700.text.

1
2


-
-
c1 c2 c3
d1  d3

Deleted tests/result/content/table/20200618140700.djson.

1

-
[{"t":"Table","p":[[[">",[{"t":"Text","s":"h1"}]],["",[{"t":"Text","s":"h2"}]],[":",[{"t":"Text","s":"h3"}]]],[[["<",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],[":",[{"t":"Text","s":"c3"}]]],[[">",[{"t":"Text","s":"f1"}]],["",[{"t":"Text","s":"f2"}]],[":",[{"t":"Text","s":"=f3"}]]]]]}]

Deleted tests/result/content/table/20200618140700.html.

1
2
3
4
5
6
7
8
9









-
-
-
-
-
-
-
-
-
<table>
<thead>
<tr><th style="text-align:right">h1</th><th>h2</th><th style="text-align:center">h3</th></tr>
</thead>
<tbody>
<tr><td style="text-align:left">c1</td><td>c2</td><td style="text-align:center">c3</td></tr>
<tr><td style="text-align:right">f1</td><td>f2</td><td style="text-align:center">=f3</td></tr>
</tbody>
</table>

Deleted tests/result/content/table/20200618140700.native.

1
2
3
4




-
-
-
-
[Table
 [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]],
 [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]],
 [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]]

Deleted tests/result/content/table/20200618140700.text.

1
2
3



-
-
-
h1 h2 h3
c1 c2 c3
f1 f2 =f3

Deleted tests/result/content/verbatim/20200215204700.djson.

1

-
[{"t":"CodeBlock","l":["if __name__ == \"main\":","  print(\"Hello, World\")","exit(0)"]}]

Deleted tests/result/content/verbatim/20200215204700.html.

1
2
3
4




-
-
-
-
<pre><code>if __name__ == &quot;main&quot;:
  print(&quot;Hello, World&quot;)
exit(0)
</code></pre>

Deleted tests/result/content/verbatim/20200215204700.native.

1

-
[CodeBlock "if __name__ == \"main\":\n  print(\"Hello, World\")\nexit(0)"]

Deleted tests/result/content/verbatim/20200215204700.text.

1
2
3



-
-
-
if __name__ == "main":
  print("Hello, World")
exit(0)

Deleted tests/result/content/verseblock/20200215204700.djson.

1

-
[{"t":"VerseBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A line"},{"t":"Hard"},{"t":"Text","s":"  another line"},{"t":"Hard"},{"t":"Text","s":"Back"}]},{"t":"Para","i":[{"t":"Text","s":"Paragraph"}]},{"t":"Para","i":[{"t":"Text","s":"    Spacy  Para"}]}],"i":[{"t":"Text","s":"Author"}]}]

Deleted tests/result/content/verseblock/20200215204700.html.

1
2
3
4
5
6
7
8








-
-
-
-
-
-
-
-
<div>
<p>A line<br>
  another line<br>
Back</p>
<p>Paragraph</p>
<p>    Spacy  Para</p>
<cite>Author</cite>
</div>

Deleted tests/result/content/verseblock/20200215204700.native.

1
2
3
4
5





-
-
-
-
-
[VerseBlock
 [[Para Text "A line",Break,Text "  another line",Break,Text "Back"],
  [Para Text "Paragraph"],
  [Para Text "    Spacy  Para"]],
 [Cite Text "Author"]]

Deleted tests/result/content/verseblock/20200215204700.text.

1
2
3
4
5
6






-
-
-
-
-
-
A line
  another line
Back
Paragraph
    Spacy  Para
Author

Changes to tests/result/meta/title/20200310110300.djson.

1


1
-
+
{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Italic","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"}
{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Emph","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"}

Changes to tests/result/meta/title/20200310110300.native.

1

2
3

1
2
3
-
+


[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Italic [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"]
[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Emph [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"]
[Role "zettel"]
[Syntax "zmk"]

Changes to tools/build.go.

127
128
129
130
131
132
133
134

135
136
137
138
139

140
141
142
143
144
145
146
147
148
149

150
151
152




153
154
155
156
157
158
159
127
128
129
130
131
132
133

134
135
136
137
138

139
140
141
142
143
144
145
146
147
148

149
150
151

152
153
154
155
156
157
158
159
160
161
162







-
+




-
+









-
+


-
+
+
+
+







func getVersion() string {
	base, vcs := getVersionData()
	return calcVersion(base, vcs)
}

func findExec(cmd string) string {
	if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" {
		return path
		return strings.TrimSpace(path)
	}
	return ""
}

func cmdCheck() error {
func cmdCheck(forRelease bool) error {
	if err := checkGoTest("./..."); err != nil {
		return err
	}
	if err := checkGoVet(); err != nil {
		return err
	}
	if err := checkGoLint(); err != nil {
		return err
	}
	if err := checkGoVetShadow(); err != nil {
	if err := checkShadow(forRelease); err != nil {
		return err
	}
	if err := checkStaticcheck(); err != nil {
	if err := checkStaticcheck(forRelease); err != nil {
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	return checkFossilExtra()
}

func checkGoTest(pkg string, testParams ...string) error {
	args := []string{"test", pkg}
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
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







-
-
+
+

-
+

-
+








+
-
-
+
+
+
+
+
+








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







	if out != "" {
		fmt.Fprintln(os.Stderr, "Some lints failed")
		fmt.Fprint(os.Stderr, out)
	}
	return err
}

func checkGoVetShadow() error {
	path := findExec("shadow")
func checkShadow(forRelease bool) error {
	path, err := findExecStrict("shadow", forRelease)
	if path == "" {
		return nil
		return err
	}
	out, err := executeCommand(nil, "go", "vet", "-vettool", strings.TrimSpace(path), "./...")
	out, err := executeCommand(nil, path, "-strict", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some shadowed variables found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkStaticcheck() error {
	out, err := executeCommand(nil, "staticcheck", "./...")
func checkStaticcheck(forRelease bool) error {
	path, err := findExecStrict("staticcheck", forRelease)
	if path == "" {
		return err
	}
	out, err := executeCommand(nil, path, "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {
		return err
	}
	out, err := executeCommand(nil, path, "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some unparam problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	if forRelease {
		if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
			fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
			if len(out2) > 0 {
				fmt.Fprintln(os.Stderr, out2)
			}
		}
	}
	return err
}

func findExecStrict(cmd string, forRelease bool) (string, error) {
	path := findExec(cmd)
	if path != "" || !forRelease {
		return path, nil
	}
	return "", errors.New("Command '" + cmd + "' not installed, but required for release")
}

func checkFossilExtra() error {
	out, err := executeCommand(nil, "fossil", "extra")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'")
		return err
	}
246
247
248
249
250
251
252
253

254
255
256
257
258
259
260
285
286
287
288
289
290
291

292
293
294
295
296
297
298
299







-
+







	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")
	err = checkGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123")
	if needServer {
		err1 := stopZettelstore(&info)
		if err == nil {
			err = err1
		}
	}
	return err
386
387
388
389
390
391
392
393

394
395
396
397
398
399
400
425
426
427
428
429
430
431

432
433
434
435
436
437
438
439







-
+







		fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil)
		base = base + dirtySuffix
	}
	return base, fossil
}

func cmdRelease() error {
	if err := cmdCheck(); err != nil {
	if err := cmdCheck(true); err != nil {
		return err
	}
	base, fossil := getReleaseVersionData()
	releases := []struct {
		arch string
		os   string
		env  []string
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
534
535
536
537
538
539
540











541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559







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







func cmdHelp() {
	fmt.Println(`Usage: go run tools/build.go [-v] COMMAND

Options:
  -v       Verbose output.

Commands:
  build    Build the software for local computer.
  check    Check current working state: execute tests, static analysis tools,
           extra files, ...
           Is automatically done when releasing the software.
  clean    Remove all build and release directories.
  help     Outputs this text.
  manual   Create a ZIP file with all manual zettel
  release  Create the software for various platforms and put them in
           appropriate named ZIP files.
  testapi  Starts a Zettelstore and execute API tests.
  version  Print the current version of the software.
  build     Build the software for local computer.
  check     Check current working state: execute tests,
            static analysis tools, extra files, ...
            Is automatically done when releasing the software.
  clean     Remove all build and release directories.
  help      Outputs this text.
  manual    Create a ZIP file with all manual zettel
  relcheck  Check current working state for release.
  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
)
534
535
536
537
538
539
540
541



542
543
544
545
546
547
548
574
575
576
577
578
579
580

581
582
583
584
585
586
587
588
589
590







-
+
+
+







		case "r", "re", "rel", "rele", "relea", "releas", "release":
			err = cmdRelease()
		case "cl", "cle", "clea", "clean":
			err = cmdClean()
		case "v", "ve", "ver", "vers", "versi", "versio", "version":
			fmt.Print(getVersion())
		case "ch", "che", "chec", "check":
			err = cmdCheck()
			err = cmdCheck(false)
		case "relc", "relch", "relche", "relchec", "relcheck":
			err = cmdCheck(true)
		case "t", "te", "tes", "test", "testa", "testap", "testapi":
			cmdTestAPI()
		case "h", "he", "hel", "help":
			cmdHelp()
		default:
			fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0])
			cmdHelp()

Changes to usecase/authenticate.go.

12
13
14
15
16
17
18

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







+







package usecase

import (
	"context"
	"math/rand"
	"time"

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

51
52
53
54
55
56
57
58
59


60
61
62
63
64
65
66



67
68
69
70
71
72
73
52
53
54
55
56
57
58


59
60
61
62
63
64



65
66
67
68
69
70
71
72
73
74







-
-
+
+




-
-
-
+
+
+







	defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond)

	if identMeta == nil || err != nil {
		compensateCompare()
		return nil, err
	}

	if hashCred, ok := identMeta.Get(meta.KeyCredential); ok {
		ok, err := cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential)
	if hashCred, ok := identMeta.Get(api.KeyCredential); ok {
		ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential)
		if err != nil {
			return nil, err
		}
		if ok {
			token, err := uc.token.GetToken(identMeta, d, k)
			if err != nil {
				return nil, err
			token, err2 := uc.token.GetToken(identMeta, d, k)
			if err2 != nil {
				return nil, err2
			}
			return token, nil
		}
		return nil, nil
	}
	compensateCompare()
	return nil, nil

Changes to usecase/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
50
51
52
53
54

55
56
57
58
59
60
61
62




63
64
65
66
67
68

69
70
71
72

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122







123
124
















125
126


































127
128











129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144








145
146

147


148
149
150
151
152
153




















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

36
37
38
39
40


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

62


63
64




65
66
67
68
69

70



71

72


73





















74
75
76
77
78














79
80
81
82
83
84




85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109


110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166






167
168
169
170
171
172
173
174


175
176
177
178






179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198







+









+
+
+
+
+
+



-
+
+



-
-
+
+



















-
+
-
-


-
-
-
-
+
+
+
+

-

-
-
-
+
-

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





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






-
-
-
-
+
+
+
+
+
+
+


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


+
+
+
+
+
+
+
+
+
+
+










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

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

// Package usecase provides (business) use cases for the Zettelstore.
package usecase

import (
	"context"

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

// ZettelContextPort is the interface used by this use case.
type ZettelContextPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

// ZettelContextConfig is the interface to allow the usecase to read some config data.
type ZettelContextConfig interface {
	// GetHomeZettel returns the value of the "home-zettel" key.
	GetHomeZettel() id.Zid
}

// ZettelContext is the data for this use case.
type ZettelContext struct {
	port ZettelContextPort
	port   ZettelContextPort
	config ZettelContextConfig
}

// NewZettelContext creates a new use case.
func NewZettelContext(port ZettelContextPort) ZettelContext {
	return ZettelContext{port: port}
func NewZettelContext(port ZettelContextPort, config ZettelContextConfig) ZettelContext {
	return ZettelContext{port: port, config: config}
}

// ZettelContextDirection determines the way, the context is calculated.
type ZettelContextDirection int

// Constant values for ZettelContextDirection
const (
	_                     ZettelContextDirection = iota
	ZettelContextForward                         // Traverse all forwarding links
	ZettelContextBackward                        // Traverse all backwaring links
	ZettelContextBoth                            // Traverse both directions
)

// Run executes the use case.
func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) {
	start, err := uc.port.GetMeta(ctx, zid)
	if err != nil {
		return nil, err
	}
	tasks := ztlCtx{depth: depth}
	tasks := newQueue(start, depth, limit, uc.config.GetHomeZettel())
	tasks.add(start, 0)
	visited := id.NewSet()
	isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward
	isForward := dir == ZettelContextBoth || dir == ZettelContextForward
	for !tasks.empty() {
		m, curDepth := tasks.pop()
		if _, ok := visited[m.Zid]; ok {
			continue
	for {
		m, curDepth, found := tasks.next()
		if !found {
			break
		}
		visited[m.Zid] = true
		result = append(result, m)
		if limit > 0 && len(result) > limit { // start is the first element of result
			break
		}

		curDepth++
		for _, p := range m.PairsRest(true) {
			if p.Key == meta.KeyBackward {
				if isBackward {
			tasks.addPair(ctx, uc.port, p.Key, p.Value, curDepth+1, isBackward, isForward)
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
				continue
			}
			if p.Key == meta.KeyForward {
				if isForward {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
				continue
			}
			if p.Key != meta.KeyBack {
				hasInverse := meta.Inverse(p.Key) != ""
				if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
					continue
				}
				if t := meta.Type(p.Key); t == meta.TypeID {
					uc.addID(ctx, &tasks, curDepth, p.Value)
				} else if t == meta.TypeIDSet {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
			}
		}
	}
	return result, nil
}

func (uc ZettelContext) addID(ctx context.Context, tasks *ztlCtx, depth int, value string) {
	if zid, err := id.Parse(value); err == nil {
		if m, err := uc.port.GetMeta(ctx, zid); err == nil {
			tasks.add(m, depth)
		}
	}
}

func (uc ZettelContext) addIDSet(ctx context.Context, tasks *ztlCtx, depth int, value string) {
	for _, val := range meta.ListFromValue(value) {
		uc.addID(ctx, tasks, depth, val)
	}
}

type ztlCtxTask struct {
	next  *ztlCtxTask
	meta  *meta.Meta
	depth int
}

type ztlCtx struct {
	first *ztlCtxTask
	last  *ztlCtxTask
	depth int
type contextQueue struct {
	home     id.Zid
	seen     id.Set
	first    *ztlCtxTask
	last     *ztlCtxTask
	maxDepth int
	limit    int
}

func newQueue(m *meta.Meta, maxDepth, limit int, home id.Zid) *contextQueue {
	task := &ztlCtxTask{
		next:  nil,
		meta:  m,
		depth: 0,
	}
	result := &contextQueue{
		home:     home,
		seen:     id.NewSet(),
		first:    task,
		last:     task,
		maxDepth: maxDepth,
		limit:    limit,
	}
	return result
}
func (zc *ztlCtx) add(m *meta.Meta, depth int) {
	if zc.depth > 0 && depth > zc.depth {

func (zc *contextQueue) addPair(
	ctx context.Context, port ZettelContextPort,
	key, value string,
	curDepth int, isBackward, isForward bool,
) {
	if key == api.KeyBackward {
		if isBackward {
			zc.addIDSet(ctx, port, curDepth, value)
		}
		return
	}
	if key == api.KeyForward {
		if isForward {
			zc.addIDSet(ctx, port, curDepth, value)
		}
		return
	}
	if key == api.KeyBack {
		return
	}
	hasInverse := meta.Inverse(key) != ""
	if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
		return
	}
	if t := meta.Type(key); t == meta.TypeID {
		zc.addID(ctx, port, curDepth, value)
	} else if t == meta.TypeIDSet {
		zc.addIDSet(ctx, port, curDepth, value)
	}
}

func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, depth int, value string) {
	if (zc.maxDepth > 0 && depth > zc.maxDepth) || zc.hasLimit() {
		return
	}

	zid, err := id.Parse(value)
	if err != nil || zid == zc.home {
		return
	}

	m, err := port.GetMeta(ctx, zid)
	if err != nil {
		return
	}

	task := &ztlCtxTask{next: nil, meta: m, depth: depth}
	if zc.first == nil {
		zc.first = task
		zc.last = task
	} else {
		zc.last.next = task
		zc.last = task
	}
}

func (zc *ztlCtx) empty() bool {
	return zc.first == nil
}

func (zc *ztlCtx) pop() (*meta.Meta, int) {
	task := zc.first
func (zc *contextQueue) addIDSet(ctx context.Context, port ZettelContextPort, curDepth int, value string) {
	for _, val := range meta.ListFromValue(value) {
		zc.addID(ctx, port, curDepth, val)
	}
}

func (zc *contextQueue) next() (*meta.Meta, int, bool) {
	if zc.hasLimit() {
	if task == nil {
		return nil, -1
		return nil, -1, false
	}
	for zc.first != nil {
		task := zc.first
	zc.first = task.next
	if zc.first == nil {
		zc.last = nil
	}
	return task.meta, task.depth
}
		zc.first = task.next
		if zc.first == nil {
			zc.last = nil
		}
		m := task.meta
		zid := m.Zid
		_, found := zc.seen[zid]
		if found {
			continue
		}
		zc.seen[zid] = true
		return m, task.depth, true
	}
	return nil, -1, false
}

func (zc *contextQueue) hasLimit() bool {
	limit := zc.limit
	return limit > 0 && len(zc.seen) > limit
}

Changes to usecase/copy_zettel.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
8
9
10
11
12
13
14


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

28
29

30
31
32
33
34
35

36
37
38
39
40
41







-
-
+
+











-
+

-
+





-
+





// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

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

// CopyZettel is the data for this use case.
type CopyZettel struct{}

// NewCopyZettel creates a new use case.
func NewCopyZettel() CopyZettel {
	return CopyZettel{}
}

// Run executes the use case.
func (uc CopyZettel) Run(origZettel domain.Zettel) domain.Zettel {
func (CopyZettel) Run(origZettel domain.Zettel) domain.Zettel {
	m := origZettel.Meta.Clone()
	if title, ok := m.Get(meta.KeyTitle); ok {
	if title, ok := m.Get(api.KeyTitle); ok {
		if len(title) > 0 {
			title = "Copy of " + title
		} else {
			title = "Copy"
		}
		m.Set(meta.KeyTitle, title)
		m.Set(api.KeyTitle, title)
	}
	content := origZettel.Content
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
}

Changes to usecase/create_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







+



-








// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

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

// CreateZettelPort is the interface used by this use case.
type CreateZettelPort interface {
	// CreateZettel creates a new zettel.
	CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error)
}
42
43
44
45
46
47
48
49
50
51


52
53
54


55
56
57


58
59
60
61
62
63
42
43
44
45
46
47
48



49
50
51


52
53
54


55
56
57
58
59
60
61
62







-
-
-
+
+

-
-
+
+

-
-
+
+







// Run executes the use case.
func (uc CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	m := zettel.Meta
	if m.Zid.IsValid() {
		return m.Zid, nil // TODO: new error: already exists
	}

	if title, ok := m.Get(meta.KeyTitle); !ok || title == "" {
		m.Set(meta.KeyTitle, uc.rtConfig.GetDefaultTitle())
	if title, ok := m.Get(api.KeyTitle); !ok || title == "" {
		m.Set(api.KeyTitle, uc.rtConfig.GetDefaultTitle())
	}
	if role, ok := m.Get(meta.KeyRole); !ok || role == "" {
		m.Set(meta.KeyRole, uc.rtConfig.GetDefaultRole())
	if role, ok := m.Get(api.KeyRole); !ok || role == "" {
		m.Set(api.KeyRole, uc.rtConfig.GetDefaultRole())
	}
	if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax())
	if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
		m.Set(api.KeySyntax, uc.rtConfig.GetDefaultSyntax())
	}
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content.TrimSpace()
	return uc.port.CreateZettel(ctx, zettel)
}

Changes to usecase/folge_zettel.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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41

42
43





44
45
46
47
48
49







+




















-
+





-
+

-
-
-
-
-
+
+
+
+
+

// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

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

// FolgeZettel is the data for this use case.
type FolgeZettel struct {
	rtConfig config.Config
}

// NewFolgeZettel creates a new use case.
func NewFolgeZettel(rtConfig config.Config) FolgeZettel {
	return FolgeZettel{rtConfig}
}

// Run executes the use case.
func (uc FolgeZettel) Run(origZettel domain.Zettel) domain.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, ok := origMeta.Get(meta.KeyTitle); ok {
	if title, ok := origMeta.Get(api.KeyTitle); ok {
		if len(title) > 0 {
			title = "Folge of " + title
		} else {
			title = "Folge"
		}
		m.Set(meta.KeyTitle, title)
		m.Set(api.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("")}
	m.Set(api.KeyRole, config.GetRole(origMeta, uc.rtConfig))
	m.Set(api.KeyTags, origMeta.GetDefault(api.KeyTags, ""))
	m.Set(api.KeySyntax, uc.rtConfig.GetDefaultSyntax())
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return domain.Zettel{Meta: m, Content: domain.NewContent(nil)}
}

Changes to usecase/get_user.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 (
	"context"

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

45
46
47
48
49
50
51
52
53
54



55
56
57
58
59
60
61
62


63
64
65
66
67
68
69
46
47
48
49
50
51
52



53
54
55
56
57
58
59
60
61


62
63
64
65
66
67
68
69
70







-
-
-
+
+
+






-
-
+
+







func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) {
	ctx = box.NoEnrichContext(ctx)

	// It is important to try first with the owner. First, because another user
	// could give herself the same ''ident''. Second, in most cases the owner
	// will authenticate.
	identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner())
	if err == nil && identMeta.GetDefault(meta.KeyUserID, "") == ident {
		if role, ok := identMeta.Get(meta.KeyRole); !ok ||
			role != meta.ValueRoleUser {
	if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident {
		if role, ok := identMeta.Get(api.KeyRole); !ok ||
			role != api.ValueRoleUser {
			return nil, nil
		}
		return identMeta, nil
	}
	// Owner was not found or has another ident. Try via list search.
	var s *search.Search
	s = s.AddExpr(meta.KeyRole, meta.ValueRoleUser)
	s = s.AddExpr(meta.KeyUserID, ident)
	s = s.AddExpr(api.KeyRole, api.ValueRoleUser)
	s = s.AddExpr(api.KeyUserID, ident)
	metaList, err := uc.port.SelectMeta(ctx, s)
	if err != nil {
		return nil, err
	}
	if len(metaList) < 1 {
		return nil, nil
	}
91
92
93
94
95
96
97
98

99
100
101
102
92
93
94
95
96
97
98

99
100
101
102
103







-
+




// 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)
	if err != nil {
		return nil, err
	}

	if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident {
	if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident {
		return nil, nil
	}
	return userMeta, nil
}

Changes to usecase/list_role.go.

11
12
13
14
15
16
17

18
19
20
21
22
23
24
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"
	"sort"

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

// ListRolePort is the interface used by this use case.
type ListRolePort interface {
40
41
42
43
44
45
46
47

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

48
49
50
51
52
53
54
55
56
57
58







-
+










func (uc ListRole) Run(ctx context.Context) ([]string, error) {
	metas, err := uc.port.SelectMeta(box.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 != "" {
		if role, ok := m.Get(api.KeyRole); ok && role != "" {
			roles[role] = true
		}
	}
	result := make([]string, 0, len(roles))
	for role := range roles {
		result = append(result, role)
	}
	sort.Strings(result)
	return result, nil
}

Changes to usecase/list_tags.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 (
	"context"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/meta"
	"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.
41
42
43
44
45
46
47
48

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

49
50
51
52
53
54
55
56







-
+







func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) {
	metas, err := uc.port.SelectMeta(ctx, nil)
	if err != nil {
		return nil, err
	}
	result := make(TagData)
	for _, m := range metas {
		if tl, ok := m.GetList(meta.KeyAllTags); ok && len(tl) > 0 {
		if tl, ok := m.GetList(api.KeyAllTags); ok && len(tl) > 0 {
			for _, t := range tl {
				result[t] = append(result[t], m)
			}
		}
	}
	if minCount > 1 {
		for t, ms := range result {

Changes to usecase/new_zettel.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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

30
31
32




33
34
35
36
37
38
39
40
41
42
43
44
45
46
47







+














-
+


-
-
-
-
+
+
+
+











// under this license.
//-----------------------------------------------------------------------------

// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"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 {
func (NewZettel) Run(origZettel domain.Zettel) domain.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta
	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, ""))
	m.Set(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	m.Set(api.KeyRole, om.GetDefault(api.KeyRole, ""))
	m.Set(api.KeyTags, om.GetDefault(api.KeyTags, ""))
	m.Set(api.KeySyntax, om.GetDefault(api.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)
		}
	}
	content := origZettel.Content
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
}

Changes to usecase/order.go.

41
42
43
44
45
46
47
48
49


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


48
49
50
51
52
53
54
55







-
-
+
+






	start *meta.Meta, result []*meta.Meta, err error,
) {
	zn, err := uc.evaluate.Run(ctx, zid, syntax, nil)
	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 {
		if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
			if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil {
				result = append(result, m)
			}
		}
	}
	return zn.Meta, result, nil
}

Changes to usecase/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







+



-








// Package usecase provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

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

// UpdateZettelPort is the interface used by this use case.
type UpdateZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)

45
46
47
48
49
50
51
52

53
54
55

56
57
58
59
60
61
62
45
46
47
48
49
50
51

52
53
54

55
56
57
58
59
60
61
62







-
+


-
+







	oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid)
	if err != nil {
		return err
	}
	if zettel.Equal(oldZettel, false) {
		return nil
	}
	m.SetNow(meta.KeyModified)
	m.SetNow(api.KeyModified)
	m.YamlSep = oldZettel.Meta.YamlSep
	if m.Zid == id.ConfigurationZid {
		m.Set(meta.KeySyntax, meta.ValueSyntaxNone)
		m.Set(api.KeySyntax, api.ValueSyntaxNone)
	}
	if !hasContent {
		zettel.Content = oldZettel.Content
		zettel.Content.TrimSpace()
	}
	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
25







-
+







// Package api provides api handlers for web requests.
package api

import (
	"context"
	"time"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/web/server"
)

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













-
+














-
+
-
-
+

-
+







//-----------------------------------------------------------------------------
// 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 "zettelstore.de/z/api"
import "zettelstore.de/c/api"

const ctHTML = "text/html; charset=utf-8"
const ctJSON = "application/json"
const ctPlainText = "text/plain; charset=utf-8"

var mapEncoding2CT = map[api.EncodingEnum]string{
	api.EncoderHTML:   ctHTML,
	api.EncoderNative: ctPlainText,
	api.EncoderDJSON:  ctJSON,
	api.EncoderText:   ctPlainText,
	api.EncoderZmk:    ctPlainText,
}

func encoding2ContentType(enc api.EncodingEnum) string {
	ct, ok := mapEncoding2CT[enc]
	if ct, ok := mapEncoding2CT[enc]; ok {
	if !ok {
		return "application/octet-stream"
		return ct
	}
	return ct
	return "application/octet-stream"
}

var mapSyntax2CT = map[string]string{
	"css":      "text/css; charset=utf-8",
	"gif":      "image/gif",
	"html":     "text/html; charset=utf-8",
	"jpeg":     "image/jpeg",

Changes to web/adapter/api/create_zettel.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
24







-
+








// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakePostCreatePlainZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
32
33
34
35
36
37
38
39

40
41
42


43
44
45
46
47
48
49
32
33
34
35
36
37
38

39



40
41
42
43
44
45
46
47
48







-
+
-
-
-
+
+







		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		u := a.NewURLBuilder('z').SetZid(newZid).String()
		u := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String())).String()
		h := w.Header()
		h.Set(zsapi.HeaderContentType, ctPlainText)
		h.Set(zsapi.HeaderLocation, u)
		h := adapter.PrepareHeader(w, ctPlainText)
		h.Set(api.HeaderLocation, u)
		w.WriteHeader(http.StatusCreated)
		if _, err = w.Write(newZid.Bytes()); err != nil {
			adapter.InternalServerError(w, "Write Plain", err)
		}
	}
}

59
60
61
62
63
64
65
66
67


68
69

70
71

72
73
74
75
58
59
60
61
62
63
64


65
66


67
68

69
70
71
72
73







-
-
+
+
-
-
+

-
+




		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		u := a.NewURLBuilder('j').SetZid(newZid).String()
		h := w.Header()
		u := a.NewURLBuilder('j').SetZid(api.ZettelID(newZid.String())).String()
		h := adapter.PrepareHeader(w, ctJSON)
		h.Set(zsapi.HeaderContentType, ctJSON)
		h.Set(zsapi.HeaderLocation, u)
		h.Set(api.HeaderLocation, u)
		w.WriteHeader(http.StatusCreated)
		if err = encodeJSONData(w, zsapi.ZidJSON{ID: newZid.String()}); err != nil {
		if err = encodeJSONData(w, api.ZidJSON{ID: api.ZettelID(newZid.String())}); err != nil {
			adapter.InternalServerError(w, "Write JSON", err)
		}
	}
}

Changes to web/adapter/api/delete_zettel.go.

24
25
26
27
28
29
30
31

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

31
32
33
34
35
36
37







-
+






	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 {
		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

Added web/adapter/api/encode_inlines.go.
































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under 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 (
	"bytes"
	"encoding/json"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakePostEncodeInlinesHandler creates a new HTTP handler to encode given
// Zettelmarkup inline material
func (a *API) MakePostEncodeInlinesHandler(evaluate usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		dec := json.NewDecoder(r.Body)
		var reqJSON api.EncodeInlineReqJSON
		if err := dec.Decode(&reqJSON); err != nil {
			http.Error(w, "Unable to read request", http.StatusBadRequest)
			return
		}
		envEnc := &encoder.Environment{
			Lang:        reqJSON.Lang,
			Interactive: reqJSON.NoLinks,
		}
		if envEnc.Lang == "" {
			envEnc.Lang = a.rtConfig.GetDefaultLang()
		}
		htmlEnc := encoder.Create(api.EncoderHTML, envEnc)
		ctx := r.Context()
		envEval := evaluator.Environment{}
		var respJSON api.EncodedInlineRespJSON
		if iln := evaluate.RunMetadata(ctx, reqJSON.FirstZmk, &envEval); iln != nil {
			s, err := encodeInlines(htmlEnc, iln)
			if err != nil {
				http.Error(w, "Unable to encode first as HTML", http.StatusBadRequest)
				return
			}
			respJSON.FirstHTML = s

			s, err = encodeInlines(encoder.Create(api.EncoderText, nil), iln)
			if err != nil {
				http.Error(w, "Unable to encode first as Text", http.StatusBadRequest)
				return
			}
			respJSON.FirstText = s
		}

		if reqLen := len(reqJSON.OtherZmk); reqLen > 0 {
			respJSON.OtherHTML = make([]string, reqLen)
			for i, zmk := range reqJSON.OtherZmk {
				iln := evaluate.RunMetadata(ctx, zmk, &envEval)
				if iln == nil {
					continue
				}
				s, err := encodeInlines(htmlEnc, iln)
				if err != nil {
					http.Error(w, "Unable to encode other as HTML", http.StatusBadRequest)
					return
				}
				respJSON.OtherHTML[i] = s
			}
		}

		adapter.PrepareHeader(w, ctJSON)
		err := encodeJSONData(w, respJSON)
		if err != nil {
			adapter.InternalServerError(w, "Write JSON for encoded Zettelmarkup", err)
		}
	}
}

func encodeInlines(encdr encoder.Encoder, inl *ast.InlineListNode) (string, error) {
	var buf bytes.Buffer
	_, err := encdr.WriteInlines(&buf, inl)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}

Changes to web/adapter/api/get_eval_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
10
11
12
13
14
15
16

17
18


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




40
41
42
43
44
45
46




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







-
+

-
-
+
+



















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











// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetEvalZettelHandler creates a new HTTP handler to return a evaluated zettel.
func (a *API) MakeGetEvalZettelHandler(evaluate usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		ctx := r.Context()
		q := r.URL.Query()
		enc, encStr := adapter.GetEncoding(r, q, encoder.GetDefaultEncoding())
		part := getPart(q, partContent)
		var embedImage bool
		if enc == zsapi.EncoderHTML {
			embedImage = true
		}
		var env evaluator.Environment
		if enc == api.EncoderHTML {
			env.GetImageMaterial = func(zettel domain.Zettel, syntax string) ast.MaterialNode {
				return &ast.BLOBMaterialNode{
					Blob:   zettel.Content.AsBytes(),
					Syntax: syntax,
				}
		env := evaluator.Environment{
			EmbedImage: embedImage,
		}
		zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), &env)
			}
		}
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), &env)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		evalMeta := func(value string) *ast.InlineListNode {
			return evaluate.RunMetadata(ctx, value, &env)
		}
		a.writeEncodedZettelPart(w, zn, evalMeta, enc, encStr, part)
	}
}

Changes to web/adapter/api/get_links.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
69
70
10
11
12
13
14
15
16

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

36
37
38
39
40
41
42

43


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

61
62
63
64
65
66
67
68







-
+


















-
+






-
+
-
-

















-
+








// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/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"
)

// MakeGetLinksHandler creates a new API handler to return links to other material.
func MakeGetLinksHandler(evaluate usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		ctx := r.Context()
		q := r.URL.Query()
		zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), nil)
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), nil)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		summary := collect.References(zn)

		outData := zsapi.ZettelLinksJSON{ID: zid.String()}
		outData := api.ZettelLinksJSON{ID: api.ZettelID(zid.String())}
		// TODO: calculate incoming links from other zettel (via "backward" metadata?)
		outData.Linked.Incoming = nil
		zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links)
		outData.Linked.Outgoing = idRefs(zetRefs)
		outData.Linked.Local = stringRefs(locRefs)
		outData.Linked.External = stringRefs(extRefs)
		for _, p := range zn.Meta.PairsRest(false) {
			if meta.Type(p.Key) == meta.TypeURL {
				outData.Linked.Meta = append(outData.Linked.Meta, p.Value)
			}
		}

		zetRefs, locRefs, extRefs = collect.DivideReferences(summary.Embeds)
		outData.Embedded.Outgoing = idRefs(zetRefs)
		outData.Embedded.Local = stringRefs(locRefs)
		outData.Embedded.External = stringRefs(extRefs)

		outData.Cites = stringCites(summary.Cites)

		w.Header().Set(zsapi.HeaderContentType, ctJSON)
		adapter.PrepareHeader(w, ctJSON)
		encodeJSONData(w, outData)
	}
}

func idRefs(refs []*ast.Reference) []string {
	result := make([]string, len(refs))
	for i, ref := range refs {

Changes to web/adapter/api/get_order.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
10
11
12
13
14
15
16


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

34
35
36
37
38
39
40
41







-
-
+
+















-
+








// 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/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetOrderHandler creates a new API handler to return zettel references
// of a given zettel.
func 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(api.KeySyntax))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		writeMetaList(w, start, metas)
	}
}

Changes to web/adapter/api/get_parsed_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

54
55
56
57
58
59
60

61
62
63
64
65
66
67

68
69
70
71
72
73
74
75
76
77
78
79
80
11
12
13
14
15
16
17

18
19
20
21

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

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

52
53
54
55
56
57
58

59
60
61
62
63
64
65

66
67
68
69
70
71
72
73
74
75
76
77
78
79







-
+



-


















-
+











-
+






-
+






-
+













// Package api provides api handlers for web requests.
package api

import (
	"fmt"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetParsedZettelHandler creates a new HTTP handler to return a parsed zettel.
func (a *API) MakeGetParsedZettelHandler(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
		}

		q := r.URL.Query()
		enc, encStr := adapter.GetEncoding(r, q, encoder.GetDefaultEncoding())
		part := getPart(q, partContent)
		zn, err := parseZettel.Run(r.Context(), zid, q.Get(meta.KeySyntax))
		zn, err := parseZettel.Run(r.Context(), zid, q.Get(api.KeySyntax))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		a.writeEncodedZettelPart(w, zn, parser.ParseMetadata, enc, encStr, part)
	}
}

func (a *API) writeEncodedZettelPart(
	w http.ResponseWriter, zn *ast.ZettelNode,
	evalMeta encoder.EvalMetaFunc,
	enc zsapi.EncodingEnum, encStr string, part partType,
	enc api.EncodingEnum, encStr string, part partType,
) {
	env := encoder.Environment{
		Lang:           config.GetLang(zn.InhMeta, a.rtConfig),
		Xhtml:          false,
		MarkerExternal: "",
		NewWindow:      false,
		IgnoreMeta:     map[string]bool{meta.KeyLang: true},
		IgnoreMeta:     map[string]bool{api.KeyLang: true},
	}
	encdr := encoder.Create(enc, &env)
	if encdr == nil {
		adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid.String(), encStr))
		return
	}
	w.Header().Set(zsapi.HeaderContentType, encoding2ContentType(enc))
	adapter.PrepareHeader(w, encoding2ContentType(enc))
	var err error
	switch part {
	case partZettel:
		_, err = encdr.WriteZettel(w, zn, evalMeta)
	case partMeta:
		_, err = encdr.WriteMeta(w, zn.InhMeta, evalMeta)
	case partContent:
		_, err = encdr.WriteContent(w, zn)
	}
	if err != nil {
		adapter.InternalServerError(w, "Write encoded zettel", err)
	}
}

Changes to web/adapter/api/get_role_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
10
11
12
13
14
15
16

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


31
32
33
34







-
+













-
-
+
+



// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel".
func 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
		}

		w.Header().Set(zsapi.HeaderContentType, ctJSON)
		encodeJSONData(w, zsapi.RoleListJSON{Roles: roleList})
		adapter.PrepareHeader(w, ctJSON)
		encodeJSONData(w, api.RoleListJSON{Roles: roleList})
	}
}

Changes to web/adapter/api/get_tags_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
11
12
13
14
15
16
17

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


33
34

35
36

37
38
39
40
41

42
43
44







-
+














-
-
+

-
+

-
+



+
-
+


// Package api provides api handlers for web requests.
package api

import (
	"net/http"
	"strconv"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel".
func 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
		}

		w.Header().Set(zsapi.HeaderContentType, ctJSON)
		tagMap := make(map[string][]string, len(tagData))
		tagMap := make(map[string][]api.ZettelID, len(tagData))
		for tag, metaList := range tagData {
			zidList := make([]string, 0, len(metaList))
			zidList := make([]api.ZettelID, 0, len(metaList))
			for _, m := range metaList {
				zidList = append(zidList, m.Zid.String())
				zidList = append(zidList, api.ZettelID(m.Zid.String()))
			}
			tagMap[tag] = zidList
		}
		adapter.PrepareHeader(w, ctJSON)
		encodeJSONData(w, zsapi.TagListJSON{Tags: tagMap})
		encodeJSONData(w, api.TagListJSON{Tags: tagMap})
	}
}

Changes to web/adapter/api/get_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
11
12
13
14
15
16
17

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

35
36


37
38
39
40
41
42
43
44
45







-
+
















-
+

-
-
+
+







// Package api provides api handlers for web requests.
package api

import (
	"context"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetZettelHandler creates a new HTTP handler to return a zettel.
func MakeGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		z, err := getZettelFromPath(r.Context(), w, r, getZettel)
		if err != nil {
			return
		}

		w.Header().Set(zsapi.HeaderContentType, ctJSON)
		adapter.PrepareHeader(w, ctJSON)
		content, encoding := z.Content.Encode()
		err = encodeJSONData(w, zsapi.ZettelJSON{
			ID:       z.Meta.Zid.String(),
		err = encodeJSONData(w, api.ZettelJSON{
			ID:       api.ZettelID(z.Meta.Zid.String()),
			Meta:     z.Meta.Map(),
			Encoding: encoding,
			Content:  content,
		})
		if err != nil {
			adapter.InternalServerError(w, "Write Zettel JSON", err)
		}
60
61
62
63
64
65
66
67

68
69
70
71

72
73
74
75
76
77
78
60
61
62
63
64
65
66

67
68
69
70

71
72
73
74
75
76
77
78







-
+



-
+







			if err == nil {
				_, err = w.Write([]byte{'\n'})
			}
			if err == nil {
				_, err = z.Content.Write(w)
			}
		case partMeta:
			w.Header().Set(zsapi.HeaderContentType, ctPlainText)
			adapter.PrepareHeader(w, ctPlainText)
			_, err = z.Meta.Write(w, false)
		case partContent:
			if ct, ok := syntax2contentType(config.GetSyntax(z.Meta, a.rtConfig)); ok {
				w.Header().Set(zsapi.HeaderContentType, ct)
				adapter.PrepareHeader(w, ct)
			}
			_, err = z.Content.Write(w)
		}
		if err != nil {
			adapter.InternalServerError(w, "Write plain zettel", err)
		}
	}
88
89
90
91
92
93
94

























88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		adapter.ReportUsecaseError(w, err)
		return domain.Zettel{}, err
	}
	return z, nil
}

// MakeGetMetaHandler creates a new HTTP handler to return metadata of a zettel.
func MakeGetMetaHandler(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
		}

		m, err := getMeta.Run(r.Context(), zid)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		adapter.PrepareHeader(w, ctJSON)
		err = encodeJSONData(w, api.MetaJSON{
			Meta: m.Map(),
		})
		if err != nil {
			adapter.InternalServerError(w, "Write Meta JSON", err)
		}
	}
}

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
49







-
+














-
-
+
+



-
+













// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
func 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 := adapter.GetZCDirection(q.Get(api.QueryKeyDir))
		depth, ok := adapter.GetInteger(q, api.QueryKeyDepth)
		if !ok || depth < 0 {
			depth = 5
		}
		limit, ok := adapter.GetInteger(q, zsapi.QueryKeyLimit)
		limit, ok := adapter.GetInteger(q, api.QueryKeyLimit)
		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
		}
		writeMetaList(w, metaList[0], metaList[1:])
	}
}

Changes to web/adapter/api/get_zettel_list.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
8
9
10
11
12
13
14
15
16
17
18

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

42
43


44
45
46
47
48
49



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

73
74
75
76
77
78
79
80
81
82
83
84







+



-
+

















+
+
+
+
+
-
+

-
-
+
+




-
-
-
+
+
+
+



















-
+











// under this license.
//-----------------------------------------------------------------------------

// Package api provides api handlers for web requests.
package api

import (
	"bytes"
	"fmt"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel".
func MakeListMetaHandler(listMeta usecase.ListMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		s := adapter.GetSearch(q)
		metaList, err := listMeta.Run(ctx, s)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		var queryText bytes.Buffer
		if s != nil {
			s.Print(&queryText)
		}

		result := make([]zsapi.ZidMetaJSON, 0, len(metaList))
		result := make([]api.ZidMetaJSON, 0, len(metaList))
		for _, m := range metaList {
			result = append(result, zsapi.ZidMetaJSON{
				ID:   m.Zid.String(),
			result = append(result, api.ZidMetaJSON{
				ID:   api.ZettelID(m.Zid.String()),
				Meta: m.Map(),
			})
		}

		w.Header().Set(zsapi.HeaderContentType, ctJSON)
		err = encodeJSONData(w, zsapi.ZettelListJSON{
			List: result,
		adapter.PrepareHeader(w, ctJSON)
		err = encodeJSONData(w, api.ZettelListJSON{
			Query: queryText.String(),
			List:  result,
		})
		if err != nil {
			adapter.InternalServerError(w, "Write Zettel list JSON", err)
		}
	}
}

// MakeListPlainHandler creates a new HTTP handler for the use case "list some zettel".
func (a *API) MakeListPlainHandler(listMeta usecase.ListMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		s := adapter.GetSearch(q)
		metaList, err := listMeta.Run(ctx, s)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		w.Header().Set(zsapi.HeaderContentType, ctPlainText)
		adapter.PrepareHeader(w, ctPlainText)
		for _, m := range metaList {
			_, err = fmt.Fprintln(w, m.Zid.String(), config.GetTitle(m, a.rtConfig))
			if err != nil {
				break
			}
		}
		if err != nil {
			adapter.InternalServerError(w, "Write Zettel list plain", err)
		}
	}
}

Changes to web/adapter/api/json.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
12
13
14
15
16
17
18

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

33
34

35
36
37



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

import (
	"encoding/json"
	"io"
	"net/http"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/web/adapter"
)

func encodeJSONData(w io.Writer, data interface{}) error {
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)
	return enc.Encode(data)
}

func writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error {
	outList := make([]zsapi.ZidMetaJSON, len(metaList))
	outList := make([]api.ZidMetaJSON, len(metaList))
	for i, m := range metaList {
		outList[i].ID = m.Zid.String()
		outList[i].ID = api.ZettelID(m.Zid.String())
		outList[i].Meta = m.Map()
	}
	w.Header().Set(zsapi.HeaderContentType, ctJSON)
	return encodeJSONData(w, zsapi.ZidMetaRelatedList{
		ID:   m.Zid.String(),
	adapter.PrepareHeader(w, ctJSON)
	return encodeJSONData(w, api.ZidMetaRelatedList{
		ID:   api.ZettelID(m.Zid.String()),
		Meta: m.Map(),
		List: outList,
	})
}

func buildZettelFromJSONData(r *http.Request, zid id.Zid) (domain.Zettel, error) {
	var zettel domain.Zettel
	dec := json.NewDecoder(r.Body)
	var zettelData zsapi.ZettelDataJSON
	var zettelData api.ZettelDataJSON
	if err := dec.Decode(&zettelData); err != nil {
		return zettel, err
	}
	m := meta.New(zid)
	for k, v := range zettelData.Meta {
		m.Set(k, v)
		m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
	}
	zettel.Meta = m
	if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil {
		return zettel, err
	}
	return zettel, nil
}

Changes to web/adapter/api/login.go.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

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

66
67
68
69
70
71
72
73
74
75
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







-
+









-
+


















-
+
















-
+



















-
+










-
+



package api

import (
	"encoding/json"
	"net/http"
	"time"

	zsapi "zettelstore.de/z/api"
	"zettelstore.de/c/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 (a *API) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			w.Header().Set(zsapi.HeaderContentType, ctJSON)
			adapter.PrepareHeader(w, ctJSON)
			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, a.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, ctJSON)
		adapter.PrepareHeader(w, ctJSON)
		writeJSONToken(w, string(token), a.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(api.AuthJSON{
		Token:   token,
		Type:    "Bearer",
		Expires: int(lifetime / time.Second),
	})
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		authData := a.getAuthData(ctx)
		if authData == nil || len(authData.Token) == 0 || authData.User == nil {
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := authData.Expires.Sub(authData.Issued)
		currentLifetime := authData.Now.Sub(authData.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			w.Header().Set(zsapi.HeaderContentType, ctJSON)
			adapter.PrepareHeader(w, ctJSON)
			writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime)
			return
		}

		// Token is a little bit aged. Create a new one
		token, err := a.getToken(authData.User)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.Header().Set(zsapi.HeaderContentType, ctJSON)
		adapter.PrepareHeader(w, ctJSON)
		writeJSONToken(w, string(token), a.tokenLifetime)
	}
}

Changes to web/adapter/api/rename_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
54
55
56
57
58
59
60
61
62
63

64
65
66

67
68
69
70
71
11
12
13
14
15
16
17

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

37
38
39
40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
55


56
57
58
59
60

61
62
63

64
65
66
67
68
69







-
+


















-
+










-
+







-
-





-
+


-
+





// Package api provides api handlers for web requests.
package api

import (
	"net/http"
	"net/url"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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 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 {
		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 {
			if zid, ok2 := getZidFromURL(value); ok2 {
				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 {
	if len(u.Path) < len(api.ZidVersion) {
		return id.Invalid, false
	}
	zid, err := id.Parse(u.Path[len(u.Path)-zidLength:])
	zid, err := id.Parse(u.Path[len(u.Path)-len(api.ZidVersion):])
	if err != nil {
		return id.Invalid, false
	}
	return zid, true
}

Changes to web/adapter/api/request.go.

12
13
14
15
16
17
18
19

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

19
20
21
22
23
24
25
26







-
+







package api

import (
	"io"
	"net/http"
	"net/url"

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

type partType int
65
66
67
68
69
70
71
72

73
74
75
76
77
78
79
65
66
67
68
69
70
71

72
73
74
75
76
77
78
79







-
+







}

func buildZettelFromPlainData(r *http.Request, zid id.Zid) (domain.Zettel, error) {
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return domain.Zettel{}, err
	}
	inp := input.NewInput(string(b))
	inp := input.NewInput(b)
	m := meta.NewFromInput(zid, inp)
	return domain.Zettel{
		Meta:    m,
		Content: domain.NewContent(inp.Src[inp.Pos:]),
	}, nil

}

Changes to web/adapter/api/update_zettel.go.

28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

57
58
59
60
61
62
28
29
30
31
32
33
34

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

56
57
58
59
60
61
62







-
+




















-
+






			return
		}
		zettel, err := buildZettelFromPlainData(r, zid)
		if err != nil {
			adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return
		}
		if err := updateZettel.Run(r.Context(), zettel, true); err != nil {
		if err = updateZettel.Run(r.Context(), zettel, true); err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func 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 := buildZettelFromJSONData(r, zid)
		if err != nil {
			adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return
		}
		if err := updateZettel.Run(r.Context(), zettel, true); err != nil {
		if err = updateZettel.Run(r.Context(), zettel, true); err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

Changes to web/adapter/request.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
28







-
+







import (
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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) {
65
66
67
68
69
70
71
72

73
74
75
76
77
78
79
65
66
67
68
69
70
71

72
73
74
75
76
77
78
79







-
+







	}
	return defEncoding, defEncoding.String()
}

func getOneEncoding(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {
		for _, value := range values {
			if enc, ok := contentType2encoding(value); ok {
			if enc, ok2 := contentType2encoding(value); ok2 {
				return enc, true
			}
		}
	}
	return "", false
}

Changes to web/adapter/response.go.

1
2

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

21
22
23
24
25
26
27









28
29
30
31
32
33
34

35
36
37
38
39
40
41
1

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

20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

-
+

















-
+



-



+
+
+
+
+
+
+
+
+







+







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

// Package adapter provides handlers for web requests.
package adapter

import (
	"errors"
	"fmt"
	"log"
	"net/http"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
)

// PrepareHeader sets the HTTP header to defined values.
func PrepareHeader(w http.ResponseWriter, contentType string) http.Header {
	h := w.Header()
	if contentType != "" {
		h.Set(api.HeaderContentType, contentType)
	}
	return h
}

// 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 {
		log.Printf("%v: %v", text, err)
	}
	// TODO: must call PrepareHeader somehow
	http.Error(w, text, code)
}

// ErrBadRequest is returned if the caller made an invalid HTTP request.
type ErrBadRequest struct {
	Text string
}
69
70
71
72
73
74
75
76

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

93
94
95
96
97
98
99
78
79
80
81
82
83
84

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

101
102
103
104
105
106
107
108







-
+















-
+







		return http.StatusConflict, "Zettelstore operations conflicted"
	}
	return http.StatusInternalServerError, err.Error()
}

// CreateTagReference builds a reference to list all tags.
func CreateTagReference(b server.Builder, key byte, enc, s string) *ast.Reference {
	u := b.NewURLBuilder(key).AppendQuery(api.QueryKeyEncoding, enc).AppendQuery(meta.KeyTags, s)
	u := b.NewURLBuilder(key).AppendQuery(api.QueryKeyEncoding, enc).AppendQuery(api.KeyTags, s)
	ref := ast.ParseReference(u.String())
	ref.State = ast.RefStateHosted
	return ref
}

// CreateHostedReference builds a reference with state "hosted".
func CreateHostedReference(b server.Builder, s string) *ast.Reference {
	urlPrefix := b.GetURLPrefix()
	ref := ast.ParseReference(urlPrefix + s)
	ref.State = ast.RefStateHosted
	return ref
}

// CreateFoundReference builds a reference for a found zettel.
func CreateFoundReference(b server.Builder, key byte, part, enc string, zid id.Zid, fragment string) *ast.Reference {
	ub := b.NewURLBuilder(key).SetZid(zid)
	ub := b.NewURLBuilder(key).SetZid(api.ZettelID(zid.String()))
	if part != "" {
		ub.AppendQuery(api.QueryKeyPart, part)
	}
	if enc != "" {
		ub.AppendQuery(api.QueryKeyEncoding, enc)
	}
	if fragment != "" {

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
12
13
14
15
16
17
18

19
20
21
22
23

24

25
26
27
28
29
30
31







-
+




-

-







package webui

import (
	"context"
	"fmt"
	"net/http"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetCopyZettelHandler creates a new HTTP handler to display the
// HTML edit view of a copied zettel.
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76







-
+







		ctx := r.Context()
		origZettel, err := getOrigZettel(ctx, 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)
		title := parser.ParseMetadata(config.GetTitle(m, wui.rtConfig))
		textTitle, err := encodeInlines(title, api.EncoderText, nil)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)}
		htmlTitle, err := encodeInlines(title, api.EncoderHTML, &env)
114
115
116
117
118
119
120
121
122


123
124
125
126
127
128
129
112
113
114
115
116
117
118


119
120
121
122
123
124
125
126
127







-
-
+
+







	ctx := r.Context()
	user := wui.getUser(ctx)
	m := zettel.Meta
	var base baseData
	wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, user, &base)
	wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
		Heading:       heading,
		MetaTitle:     m.GetDefault(meta.KeyTitle, ""),
		MetaTags:      m.GetDefault(meta.KeyTags, ""),
		MetaTitle:     m.GetDefault(api.KeyTitle, ""),
		MetaTags:      m.GetDefault(api.KeyTags, ""),
		MetaRole:      config.GetRole(m, wui.rtConfig),
		MetaSyntax:    config.GetSyntax(m, wui.rtConfig),
		MetaPairsRest: m.PairsRest(false),
		IsTextContent: !zettel.Content.IsBinary(),
		Content:       zettel.Content.AsString(),
	})
}
144
145
146
147
148
149
150
151

152
153
142
143
144
145
146
147
148

149
150
151







-
+


		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid))
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
	}
}

Changes to web/adapter/webui/delete_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
63
64
65
66
67
68
69
70
71
72
73
74

75
76
77
78
79
80


































10
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

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

66
67
68


69
70
71
72
73
74
75


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136







+

-
+










-
+
+
+
+
+














-
+




+

+
+
+
+
+
+
+
+
+

-



-
-
+
+
+
+
+
+

-
-
+
+
+
+
+
+














-
+






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

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"net/http"
	"sort"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/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 {
func (wui *WebUI) MakeGetDeleteZettelHandler(
	getMeta usecase.GetMeta,
	getAllMeta usecase.GetAllMeta,
	evaluate *usecase.Evaluate,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if enc, encText := adapter.GetEncoding(r, r.URL.Query(), api.EncoderHTML); enc != api.EncoderHTML {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Delete zettel not possible in encoding %q", encText)))
			return
		}

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		zettel, err := getZettel.Run(ctx, zid)
		ms, err := getAllMeta.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := ms[0]

		var shadowedBox string
		var incomingLinks []simpleLink
		if len(ms) > 1 {
			shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???")
		} else {
			getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
			incomingLinks = wui.encodeIncoming(m, getTextTitle)
		}

		user := wui.getUser(ctx)
		m := zettel.Meta
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), user, &base)
		wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct {
			Zid       string
			MetaPairs []meta.Pair
			Zid         string
			MetaPairs   []meta.Pair
			HasShadows  bool
			ShadowedBox string
			HasIncoming bool
			Incoming    []simpleLink
		}{
			Zid:       zid.String(),
			MetaPairs: m.Pairs(true),
			Zid:         zid.String(),
			MetaPairs:   m.Pairs(true),
			HasShadows:  shadowedBox != "",
			ShadowedBox: shadowedBox,
			HasIncoming: len(incomingLinks) > 0,
			Incoming:    incomingLinks,
		})
	}
}

// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		if err := deleteZettel.Run(r.Context(), zid); err != nil {
		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}

func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) []simpleLink {
	zidMap := make(map[string]bool)
	addListValues(zidMap, m, api.KeyBackward)
	for _, kd := range meta.GetSortedKeyDescriptions() {
		inverseKey := kd.Inverse
		if inverseKey == "" {
			continue
		}
		ikd := meta.GetDescription(inverseKey)
		switch ikd.Type {
		case meta.TypeID:
			if val, ok := m.Get(inverseKey); ok {
				zidMap[val] = true
			}
		case meta.TypeIDSet:
			addListValues(zidMap, m, inverseKey)
		}
	}
	values := make([]string, 0, len(zidMap))
	for val := range zidMap {
		values = append(values, val)
	}
	sort.Strings(values)
	return wui.encodeZidLinks(values, getTextTitle)
}

func addListValues(zidMap map[string]bool, m *meta.Meta, key string) {
	if values, ok := m.GetList(key); ok {
		for _, val := range values {
			zidMap[val] = true
		}
	}
}

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
11
12
13
14
15
16
17

18
19
20
21

22
23
24
25
26
27
28







-
+



-







// Package webui provides web-UI handlers for web requests.
package webui

import (
	"fmt"
	"net/http"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/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 {
49
50
51
52
53
54
55
56
57
58
59




60
61
62
63
64
65
66
48
49
50
51
52
53
54




55
56
57
58
59
60
61
62
63
64
65







-
-
-
-
+
+
+
+








		user := wui.getUser(ctx)
		m := zettel.Meta
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base)
		wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
			Heading:       base.Title,
			MetaTitle:     m.GetDefault(meta.KeyTitle, ""),
			MetaRole:      m.GetDefault(meta.KeyRole, ""),
			MetaTags:      m.GetDefault(meta.KeyTags, ""),
			MetaSyntax:    m.GetDefault(meta.KeySyntax, ""),
			MetaTitle:     m.GetDefault(api.KeyTitle, ""),
			MetaRole:      m.GetDefault(api.KeyRole, ""),
			MetaTags:      m.GetDefault(api.KeyTags, ""),
			MetaSyntax:    m.GetDefault(api.KeySyntax, ""),
			MetaPairsRest: m.PairsRest(false),
			IsTextContent: !zettel.Content.IsBinary(),
			Content:       zettel.Content.AsString(),
		})
	}
}

77
78
79
80
81
82
83
84

85
86
87
88

89
90
76
77
78
79
80
81
82

83
84
85
86

87
88
89







-
+



-
+



		zettel, hasContent, err := parseZettelForm(r, zid)
		if err != nil {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form"))
			return
		}

		if err := updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
		if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid))
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())))
	}
}

Changes to web/adapter/webui/forms.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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55

56
57
58


59
60
61
62
63

64
65
66

67
68
69
70
71
72

73
74
75
76
77
78
79
80

81
82
83
84
85
86
87
88
89
90
91
92
93







+



+
















+
+
+
+
+









-
+
+




-
+


-
-
+
+



-
+


-
+





-
+
+
+
+




-
+












// under this license.
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"net/http"
	"strings"

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

type formZettelData struct {
	Heading       string
	MetaTitle     string
	MetaRole      string
	MetaTags      string
	MetaSyntax    string
	MetaPairsRest []meta.Pair
	IsTextContent bool
	Content       string
}

var (
	bsCRLF = []byte{'\r', '\n'}
	bsLF   = []byte{'\n'}
)

func parseZettelForm(r *http.Request, zid id.Zid) (domain.Zettel, bool, error) {
	err := r.ParseForm()
	if err != nil {
		return domain.Zettel{}, false, err
	}

	var m *meta.Meta
	if postMeta, ok := trimmedFormValue(r, "meta"); ok {
		m = meta.NewFromInput(zid, input.NewInput(postMeta))
		m = meta.NewFromInput(zid, input.NewInput([]byte(postMeta)))
		m.Sanitize()
	} else {
		m = meta.New(zid)
	}
	if postTitle, ok := trimmedFormValue(r, "title"); ok {
		m.Set(meta.KeyTitle, postTitle)
		m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
	}
	if postTags, ok := trimmedFormValue(r, "tags"); ok {
		if tags := strings.Fields(postTags); len(tags) > 0 {
			m.SetList(meta.KeyTags, tags)
		if tags := strings.Fields(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
			m.SetList(api.KeyTags, tags)
		}
	}
	if postRole, ok := trimmedFormValue(r, "role"); ok {
		m.Set(meta.KeyRole, postRole)
		m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole))
	}
	if postSyntax, ok := trimmedFormValue(r, "syntax"); ok {
		m.Set(meta.KeySyntax, postSyntax)
		m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
	}
	if values, ok := r.PostForm["content"]; ok && len(values) > 0 {
		return domain.Zettel{
			Meta: m,
			Content: domain.NewContent(
				strings.ReplaceAll(strings.TrimSpace(values[0]), "\r\n", "\n")),
				bytes.ReplaceAll(
					bytes.TrimSpace([]byte(values[0])),
					bsCRLF,
					bsLF)),
		}, true, nil
	}
	return domain.Zettel{
		Meta:    m,
		Content: domain.NewContent(""),
		Content: domain.NewContent(nil),
	}, false, nil
}

func trimmedFormValue(r *http.Request, key string) (string, bool) {
	if values, ok := r.PostForm[key]; ok && len(values) > 0 {
		value := strings.TrimSpace(values[0])
		if len(value) > 0 {
			return value, true
		}
	}
	return "", false
}

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

20

21
22
23
24
25


26
27
28
29
30
31
32
33
34







+




-

-
+




-
-
+
+







// under this license.
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"sort"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

type metaDataInfo struct {
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85



86
87
88
89
90
91
92
93
94
95









96
97
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121







-
+









-









+
+
+







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










+








		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax))
		zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		summary := collect.References(zn)
		locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Embeds...))

		envEval := evaluator.Environment{
			EmbedImage: true,
			GetTagRef: func(s string) *ast.Reference {
				return adapter.CreateTagReference(wui, 'h', api.EncodingHTML, s)
			},
			GetHostedRef: func(s string) *ast.Reference {
				return adapter.CreateHostedReference(wui, s)
			},
			GetFoundRef: func(zid id.Zid, fragment string) *ast.Reference {
				return adapter.CreateFoundReference(wui, 'h', "", "", zid, fragment)
			},
			GetImageMaterial: func(zettel domain.Zettel, _ string) ast.MaterialNode {
				return wui.createImageMaterial(zettel.Meta.Zid)
			},
		}
		lang := config.GetLang(zn.InhMeta, wui.rtConfig)
		envHTML := encoder.Environment{Lang: lang}
		pairs := zn.Meta.Pairs(true)
		metaData := make([]metaDataInfo, len(pairs))
		getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
		for i, p := range pairs {
			var html strings.Builder
			wui.writeHTMLMetaValue(ctx, &html, p.Key, p.Value, getTextTitle, evaluate, &envEval, &envHTML)
			metaData[i] = metaDataInfo{p.Key, html.String()}
			var buf bytes.Buffer
			wui.writeHTMLMetaValue(
				&buf, p.Key, p.Value,
				getTextTitle,
				func(val string) *ast.InlineListNode {
					return evaluate.RunMetadata(ctx, val, &envEval)
				},
				&envHTML)
			metaData[i] = metaDataInfo{p.Key, buf.String()}
		}
		shadowLinks := getShadowLinks(ctx, zid, getAllMeta)
		endnotes, err := encodeBlocks(&ast.BlockListNode{}, api.EncoderHTML, &envHTML)
		if err != nil {
			endnotes = ""
		}

		textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate)
		user := wui.getUser(ctx)
		canCreate := wui.canCreate(ctx, user)
		apiZid := api.ZettelID(zid.String())
		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
129
130
131
132
133
134
135
136
137


138
139

140
141

142
143

144
145

146
147

148
149
150
151
152
153
154
138
139
140
141
142
143
144


145
146
147

148
149

150
151

152
153

154
155

156
157
158
159
160
161
162
163







-
-
+
+

-
+

-
+

-
+

-
+

-
+







			EvalMatrix     []matrixLine
			ParseMatrix    []matrixLine
			HasShadowLinks bool
			ShadowLinks    []string
			Endnotes       string
		}{
			Zid:            zid.String(),
			WebURL:         wui.NewURLBuilder('h').SetZid(zid).String(),
			ContextURL:     wui.NewURLBuilder('k').SetZid(zid).String(),
			WebURL:         wui.NewURLBuilder('h').SetZid(apiZid).String(),
			ContextURL:     wui.NewURLBuilder('k').SetZid(apiZid).String(),
			CanWrite:       wui.canWrite(ctx, user, zn.Meta, zn.Content),
			EditURL:        wui.NewURLBuilder('e').SetZid(zid).String(),
			EditURL:        wui.NewURLBuilder('e').SetZid(apiZid).String(),
			CanFolge:       canCreate,
			FolgeURL:       wui.NewURLBuilder('f').SetZid(zid).String(),
			FolgeURL:       wui.NewURLBuilder('f').SetZid(apiZid).String(),
			CanCopy:        canCreate && !zn.Content.IsBinary(),
			CopyURL:        wui.NewURLBuilder('c').SetZid(zid).String(),
			CopyURL:        wui.NewURLBuilder('c').SetZid(apiZid).String(),
			CanRename:      wui.canRename(ctx, user, zn.Meta),
			RenameURL:      wui.NewURLBuilder('b').SetZid(zid).String(),
			RenameURL:      wui.NewURLBuilder('b').SetZid(apiZid).String(),
			CanDelete:      wui.canDelete(ctx, user, zn.Meta),
			DeleteURL:      wui.NewURLBuilder('d').SetZid(zid).String(),
			DeleteURL:      wui.NewURLBuilder('d').SetZid(apiZid).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),
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
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







-
+

-
+

















+


-
+
-
-
+




-
+

+
+


+
+
+
+








-
+





	encodings := encoder.GetEncodings()
	encTexts := make([]string, 0, len(encodings))
	for _, f := range encodings {
		encTexts = append(encTexts, f.String())
	}
	sort.Strings(encTexts)
	defEncoding := encoder.GetDefaultEncoding().String()
	parts := []string{api.PartZettel, api.PartMeta, api.PartContent}
	parts := getParts()
	matrix := make([]matrixLine, 0, len(parts))
	u := wui.NewURLBuilder(key).SetZid(zid)
	u := wui.NewURLBuilder(key).SetZid(api.ZettelID(zid.String()))
	for _, part := range parts {
		row := make([]simpleLink, len(encTexts))
		for j, enc := range encTexts {
			u.AppendQuery(api.QueryKeyPart, part)
			if enc != defEncoding {
				u.AppendQuery(api.QueryKeyEncoding, enc)
			}
			row[j] = simpleLink{enc, u.String()}
			u.ClearQuery()
		}
		matrix = append(matrix, matrixLine{part, row})
	}
	return matrix
}

func (wui *WebUI) infoAPIMatrixPlain(key byte, zid id.Zid) []matrixLine {
	matrix := wui.infoAPIMatrix(key, zid)
	apiZid := api.ZettelID(zid.String())

	// Append plain and JSON format
	u := wui.NewURLBuilder('z').SetZid(zid)
	u := wui.NewURLBuilder('z').SetZid(apiZid)
	parts := []string{api.PartZettel, api.PartMeta, api.PartContent}
	for i, part := range parts {
	for i, part := range getParts() {
		u.AppendQuery(api.QueryKeyPart, part)
		matrix[i].Elements = append(matrix[i].Elements, simpleLink{"plain", u.String()})
		u.ClearQuery()
	}
	u = wui.NewURLBuilder('j').SetZid(zid)
	u = wui.NewURLBuilder('j').SetZid(apiZid)
	matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.String()})
	u = wui.NewURLBuilder('m').SetZid(apiZid)
	matrix[1].Elements = append(matrix[1].Elements, simpleLink{"json", u.String()})
	return matrix
}

func getParts() []string {
	return []string{api.PartZettel, api.PartMeta, api.PartContent}
}

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 {
		if boxNo, ok := m.Get(api.KeyBoxNumber); ok {
			result = append(result, boxNo)
		}
	}
	return result
}

Changes to web/adapter/webui/get_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



54
55

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

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

87
88
89
90
91
92




93
94
95
96
97
98
99
11
12
13
14
15
16
17

18

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

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

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

73
74
75
76
77
78
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 webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"errors"
	"net/http"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"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(evaluate *usecase.Evaluate, 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)
			return
		}

		q := r.URL.Query()
		env := evaluator.Environment{
			EmbedImage: true,
			GetTagRef: func(s string) *ast.Reference {
				return adapter.CreateTagReference(wui, 'h', api.EncodingHTML, s)
			},
			GetHostedRef: func(s string) *ast.Reference {
				return adapter.CreateHostedReference(wui, s)
			},
			GetFoundRef: func(zid id.Zid, fragment string) *ast.Reference {
				return adapter.CreateFoundReference(wui, 'h', "", "", zid, fragment)
			},
			GetImageMaterial: func(zettel domain.Zettel, _ string) ast.MaterialNode {
				return wui.createImageMaterial(zettel.Meta.Zid)
			},
		}
		zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), &env)
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), &env)

		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		evalMeta := func(value string) *ast.InlineListNode {
			return evaluate.RunMetadata(ctx, value, &env)
		}
		lang := config.GetLang(zn.InhMeta, wui.rtConfig)
		envHTML := encoder.Environment{
			Lang:           lang,
			Xhtml:          false,
			MarkerExternal: wui.rtConfig.GetMarkerExternal(),
			NewWindow:      true,
			IgnoreMeta:     map[string]bool{meta.KeyTitle: true, meta.KeyLang: true},
			IgnoreMeta:     map[string]bool{api.KeyTitle: true, api.KeyLang: true},
		}
		metaHeader, err := encodeMeta(zn.InhMeta, evalMeta, api.EncoderHTML, &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate)
		htmlTitle := wui.encodeTitleAsHTML(ctx, zn.InhMeta, evaluate, &env, &envHTML)
		htmlContent, err := encodeBlocks(zn.Ast, api.EncoderHTML, &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		user := wui.getUser(ctx)
		roleText := zn.Meta.GetDefault(meta.KeyRole, "*")
		roleText := zn.Meta.GetDefault(api.KeyRole, "*")
		tags := wui.buildTagInfos(zn.Meta)
		canCreate := wui.canCreate(ctx, user)
		getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
		extURL, hasExtURL := zn.Meta.Get(meta.KeyURL)
		folgeLinks := wui.encodeZettelLinks(zn.InhMeta, meta.KeyFolge, getTextTitle)
		backLinks := wui.encodeZettelLinks(zn.InhMeta, meta.KeyBack, getTextTitle)
		extURL, hasExtURL := zn.Meta.Get(api.KeyURL)
		folgeLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle)
		backLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle)
		apiZid := api.ZettelID(zid.String())
		var base baseData
		wui.makeBaseData(ctx, lang, textTitle, user, &base)
		base.MetaHeader = metaHeader
		wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
			HTMLTitle     string
			CanWrite      bool
			EditURL       string
115
116
117
118
119
120
121
122

123
124

125
126
127
128
129
130

131
132
133


134
135
136
137
138
139
140
118
119
120
121
122
123
124

125
126

127
128
129
130
131
132

133
134


135
136
137
138
139
140
141
142
143







-
+

-
+





-
+

-
-
+
+







			HasFolgeLinks bool
			FolgeLinks    []simpleLink
			HasBackLinks  bool
			BackLinks     []simpleLink
		}{
			HTMLTitle:     htmlTitle,
			CanWrite:      wui.canWrite(ctx, user, zn.Meta, zn.Content),
			EditURL:       wui.NewURLBuilder('e').SetZid(zid).String(),
			EditURL:       wui.NewURLBuilder('e').SetZid(apiZid).String(),
			Zid:           zid.String(),
			InfoURL:       wui.NewURLBuilder('i').SetZid(zid).String(),
			InfoURL:       wui.NewURLBuilder('i').SetZid(apiZid).String(),
			RoleText:      roleText,
			RoleURL:       wui.NewURLBuilder('h').AppendQuery("role", roleText).String(),
			HasTags:       len(tags) > 0,
			Tags:          tags,
			CanCopy:       canCreate && !zn.Content.IsBinary(),
			CopyURL:       wui.NewURLBuilder('c').SetZid(zid).String(),
			CopyURL:       wui.NewURLBuilder('c').SetZid(apiZid).String(),
			CanFolge:      canCreate,
			FolgeURL:      wui.NewURLBuilder('f').SetZid(zid).String(),
			PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, meta.KeyPrecursor, getTextTitle),
			FolgeURL:      wui.NewURLBuilder('f').SetZid(apiZid).String(),
			PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle),
			ExtURL:        extURL,
			HasExtURL:     hasExtURL,
			ExtNewWindow:  htmlAttrNewWindow(envHTML.NewWindow && hasExtURL),
			Content:       htmlContent,
			HasFolgeLinks: len(folgeLinks) > 0,
			FolgeLinks:    folgeLinks,
			HasBackLinks:  len(backLinks) > 0,
152
153
154
155
156
157
158
159
160


161
162
163
164

165
166
167
168
169
170
171
172
173
174


175
176
177
178

179
180
181
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
155
156
157
158
159
160
161


162
163
164
165
166

167
168
169
170
171
172
173
174
175


176
177
178
179
180

181
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







-
-
+
+



-
+








-
-
+
+



-
+











-
-
+
+



-
+




-
+







		return "", nil
	}
	encdr := encoder.Create(enc, env)
	if encdr == nil {
		return "", errNoSuchEncoding
	}

	var content strings.Builder
	_, err := encdr.WriteInlines(&content, is)
	var buf bytes.Buffer
	_, err := encdr.WriteInlines(&buf, is)
	if err != nil {
		return "", err
	}
	return content.String(), nil
	return buf.String(), nil
}

func encodeBlocks(bln *ast.BlockListNode, enc api.EncodingEnum, env *encoder.Environment) (string, error) {
	encdr := encoder.Create(enc, env)
	if encdr == nil {
		return "", errNoSuchEncoding
	}

	var content strings.Builder
	_, err := encdr.WriteBlocks(&content, bln)
	var buf bytes.Buffer
	_, err := encdr.WriteBlocks(&buf, bln)
	if err != nil {
		return "", err
	}
	return content.String(), nil
	return buf.String(), nil
}

func encodeMeta(
	m *meta.Meta, evalMeta encoder.EvalMetaFunc,
	enc api.EncodingEnum, env *encoder.Environment,
) (string, error) {
	encdr := encoder.Create(enc, env)
	if encdr == nil {
		return "", errNoSuchEncoding
	}

	var content strings.Builder
	_, err := encdr.WriteMeta(&content, m, evalMeta)
	var buf bytes.Buffer
	_, err := encdr.WriteMeta(&buf, m, evalMeta)
	if err != nil {
		return "", err
	}
	return content.String(), nil
	return buf.String(), nil
}

func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink {
	var tagInfos []simpleLink
	if tags, ok := m.GetList(meta.KeyTags); ok {
	if tags, ok := m.GetList(api.KeyTags); ok {
		ub := wui.NewURLBuilder('h')
		tagInfos = make([]simpleLink, len(tags))
		for i, tag := range tags {
			tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery("tags", tag).String()}
			ub.ClearQuery()
		}
	}
218
219
220
221
222
223
224




225
226
227
228
229
230
231
232

233
234
235
236
237
238
239
240
241
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







+
+
+
+







-
+









}

func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink {
	values, ok := m.GetList(key)
	if !ok || len(values) == 0 {
		return nil
	}
	return wui.encodeZidLinks(values, getTextTitle)
}

func (wui *WebUI) encodeZidLinks(values []string, getTextTitle getTextTitleFunc) []simpleLink {
	result := make([]simpleLink, 0, len(values))
	for _, val := range values {
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTextTitle(zid); found > 0 {
			url := wui.NewURLBuilder('h').SetZid(zid).String()
			url := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).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
57
58







+



















+


-
+






-
+









package webui

import (
	"context"
	"errors"
	"net/http"

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

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)
			return
		}
		homeZid := wui.rtConfig.GetHomeZettel()
		apiHomeZid := api.ZettelID(homeZid.String())
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetMeta(ctx, homeZid); err == nil {
				redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
				redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
				return
			}
			homeZid = id.DefaultHomeZid
		}
		_, err := s.GetMeta(ctx, homeZid)
		if err == nil {
			redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
			redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
			return
		}
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil {
			redirectFound(w, r, wui.NewURLBuilder('i'))
			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


35
36
37


38
39

40
41
42
43
44
45
46
15
16
17
18
19
20
21

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


39
40
41

42
43
44
45
46
47
48
49







-
+
+












+
+

-
-
+
+

-
+







	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"time"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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/evaluator"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
)

var space = []byte{' '}

type evalMetadataFunc = func(string) *ast.InlineListNode

func (wui *WebUI) writeHTMLMetaValue(
	ctx context.Context,
	w io.Writer, key, value string,
	w io.Writer,
	key, value string,
	getTextTitle getTextTitleFunc,
	evaluate *usecase.Evaluate, envEval *evaluator.Environment,
	evalMetadata evalMetadataFunc,
	envEnc *encoder.Environment,
) {
	switch kt := meta.Type(key); kt {
	case meta.TypeBool:
		wui.writeHTMLBool(w, key, value)
	case meta.TypeCredential:
		writeCredential(w, value)
63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80







-
+







	case meta.TypeURL:
		writeURL(w, value)
	case meta.TypeWord:
		wui.writeWord(w, key, value)
	case meta.TypeWordSet:
		wui.writeWordSet(w, key, meta.ListFromValue(value))
	case meta.TypeZettelmarkup:
		io.WriteString(w, encodeZmkMetadata(ctx, value, evaluate, envEval, api.EncoderHTML, envEnc))
		io.WriteString(w, encodeZmkMetadata(value, evalMetadata, api.EncoderHTML, envEnc))
	default:
		strfun.HTMLEscape(w, value, false)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
	}
}

func (wui *WebUI) writeHTMLBool(w io.Writer, key, value string) {
95
96
97
98
99
100
101

102
103

104
105

106
107
108
109
110
111
112
98
99
100
101
102
103
104
105
106

107
108

109
110
111
112
113
114
115
116







+

-
+

-
+







	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
		if title == "" {
			fmt.Fprintf(w, "<a href=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), zid)
			fmt.Fprintf(w, "<a href=\"%v\">%v</a>", ub, zid)
		} else {
			fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), title, zid)
			fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", ub, title, zid)
		}
	case found == 0:
		fmt.Fprintf(w, "<s>%v</s>", val)
	case found < 0:
		io.WriteString(w, val)
	}
}
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
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







-
+
+
+
+
+
+






-
+
+
+
+
+
+



-
+
-


-
+




-
-
+
+

-
+


func (wui *WebUI) encodeTitleAsHTML(
	ctx context.Context, m *meta.Meta,
	evaluate *usecase.Evaluate, envEval *evaluator.Environment,
	envHTML *encoder.Environment,
) string {
	plainTitle := config.GetTitle(m, wui.rtConfig)
	return encodeZmkMetadata(ctx, plainTitle, evaluate, envEval, api.EncoderHTML, envHTML)
	return encodeZmkMetadata(
		plainTitle,
		func(val string) *ast.InlineListNode {
			return evaluate.RunMetadata(ctx, plainTitle, envEval)
		},
		api.EncoderHTML, envHTML)
}

func (wui *WebUI) encodeTitleAsText(
	ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate,
) string {
	plainTitle := config.GetTitle(m, wui.rtConfig)
	return encodeZmkMetadata(ctx, plainTitle, evaluate, nil, api.EncoderText, nil)
	return encodeZmkMetadata(
		plainTitle,
		func(val string) *ast.InlineListNode {
			return evaluate.RunMetadata(ctx, plainTitle, nil)
		},
		api.EncoderText, nil)
}

func encodeZmkMetadata(
	ctx context.Context, value string,
	value string, evalMetadata evalMetadataFunc,
	evaluate *usecase.Evaluate, envEval *evaluator.Environment,
	enc api.EncodingEnum, envHTML *encoder.Environment,
) string {
	iln := evaluate.RunMetadata(ctx, value, envEval)
	iln := evalMetadata(value)
	if iln.IsEmpty() {
		return ""
	}
	result, err := encodeInlines(iln, enc, envHTML)
	if err == nil {
		return result
	if err != nil {
		return err.Error()
	}
	return err.Error()
	return result
}

Changes to web/adapter/webui/lists.go.

8
9
10
11
12
13
14

15
16
17
18
19
20
21
22

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

21

22
23
24
25
26
27
28
29







+





-

-
+







// under this license.
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/search"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
109
110
111
112
113
114
115
116

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

116
117
118
119
120
121
122
123







-
+







	Name   string
	URL    string
	iCount int
	Count  string
	Size   string
}

var fontSizes = [...]int{75, 83, 100, 117, 150, 200}
const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css

func (wui *WebUI) renderTagsList(w http.ResponseWriter, r *http.Request, listTags usecase.ListTags) {
	ctx := r.Context()
	iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min"))
	tagData, err := listTags.Run(ctx, iMinCount)
	if err != nil {
		wui.reportError(ctx, w, err)
140
141
142
143
144
145
146
147

148
149
150
151
152
153
154
140
141
142
143
144
145
146

147
148
149
150
151
152
153
154







-
+








	countList := make([]int, 0, len(countMap))
	for count := range countMap {
		countList = append(countList, count)
	}
	sort.Ints(countList)
	for pos, count := range countList {
		countMap[count] = fontSizes[(pos*len(fontSizes))/len(countList)]
		countMap[count] = (pos * fontSizes) / len(countList)
	}
	for i := 0; i < len(tagsList); i++ {
		count := tagsList[i].iCount
		tagsList[i].Count = strconv.Itoa(count)
		tagsList[i].Size = strconv.Itoa(countMap[count])
	}

209
210
211
212
213
214
215

216
217
218
219

220
221
222
223
224
225
226
209
210
211
212
213
214
215
216
217
218
219

220
221
222
223
224
225
226
227







+



-
+







		depth := getIntParameter(q, api.QueryKeyDepth, 5)
		limit := getIntParameter(q, api.QueryKeyLimit, 200)
		metaList, err := getContext.Run(ctx, zid, dir, depth, limit)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		apiZid := api.ZettelID(zid.String())
		metaLinks := wui.buildHTMLMetaList(ctx, metaList, evaluate)
		depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"}
		depthLinks := make([]simpleLink, len(depths))
		depthURL := wui.NewURLBuilder('k').SetZid(zid)
		depthURL := wui.NewURLBuilder('k').SetZid(apiZid)
		for i, depth := range depths {
			depthURL.ClearQuery()
			switch dir {
			case usecase.ZettelContextBackward:
				depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward)
			case usecase.ZettelContextForward:
				depthURL.AppendQuery(api.QueryKeyDir, api.DirForward)
236
237
238
239
240
241
242
243

244
245
246
247
248
249
250
237
238
239
240
241
242
243

244
245
246
247
248
249
250
251







-
+







			Title   string
			InfoURL string
			Depths  []simpleLink
			Start   simpleLink
			Metas   []simpleLink
		}{
			Title:   "Zettel Context",
			InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(),
			InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(),
			Depths:  depthLinks,
			Start:   metaLinks[0],
			Metas:   metaLinks[1:],
		})
	}
}

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







-
-
+
+

-
-
+
+

-
+










-
+







-
+




	})
}

func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string {
	if s == nil {
		return wui.rtConfig.GetSiteName()
	}
	var sb strings.Builder
	sb.WriteString(prefix)
	var buf bytes.Buffer
	buf.WriteString(prefix)
	if s != nil {
		sb.WriteString(": ")
		s.Print(&sb)
		buf.WriteString(": ")
		s.Print(&buf)
	}
	return sb.String()
	return buf.String()
}

// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering.
func (wui *WebUI) buildHTMLMetaList(
	ctx context.Context, metaList []*meta.Meta, evaluate *usecase.Evaluate,
) []simpleLink {
	defaultLang := wui.rtConfig.GetDefaultLang()
	metas := make([]simpleLink, 0, len(metaList))
	for _, m := range metaList {
		var lang string
		if val, ok := m.Get(meta.KeyLang); ok {
		if val, ok := m.Get(api.KeyLang); ok {
			lang = val
		} else {
			lang = defaultLang
		}
		env := encoder.Environment{Lang: lang, Interactive: true}
		metas = append(metas, simpleLink{
			Text: wui.encodeTitleAsHTML(ctx, m, evaluate, nil, &env),
			URL:  wui.NewURLBuilder('h').SetZid(m.Zid).String(),
			URL:  wui.NewURLBuilder('h').SetZid(api.ZettelID(m.Zid.String())).String(),
		})
	}
	return metas
}

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




57
58
59




60
61
62
63
64
65
66
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 webui

import (
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/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 {
func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, evaluate *usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		m, err := getMeta.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if enc, encText := adapter.GetEncoding(r, r.URL.Query(), api.EncoderHTML); enc != api.EncoderHTML {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Rename zettel %q not possible in encoding %q", zid.String(), encText)))
			return
		}

		getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
		incomingLinks := wui.encodeIncoming(m, getTextTitle)

		user := wui.getUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), user, &base)
		wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct {
			Zid       string
			MetaPairs []meta.Pair
			Zid         string
			MetaPairs   []meta.Pair
			HasIncoming bool
			Incoming    []simpleLink
		}{
			Zid:       zid.String(),
			MetaPairs: m.Pairs(true),
			Zid:         zid.String(),
			MetaPairs:   m.Pairs(true),
			HasIncoming: len(incomingLinks) > 0,
			Incoming:    incomingLinks,
		})
	}
}

// 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) {
76
77
78
79
80
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

98
99
100
101

102
103
104







-
+
+

+
-
+



-
+



-
+


			return
		}
		if formCurZid, err1 := id.Parse(
			r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form"))
			return
		}
		newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid")))
		formNewZid := strings.TrimSpace(r.PostFormValue("newzid"))
		newZid, err := id.Parse(formNewZid)
		if err != nil {
			wui.reportError(
			wui.reportError(ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", newZid)))
				ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid)))
			return
		}

		if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil {
		if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid))
		redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
	}
}

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
23
24
25
26
27
28
29
30
31







+
+
-
+





+
+
+
+
+
+
+

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"net/http"

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

func redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
	http.Redirect(w, r, ub.String(), http.StatusFound)
}

func (wui *WebUI) createImageMaterial(zid id.Zid) ast.MaterialNode {
	ub := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))
	ref := ast.ParseReference(ub.String())
	ref.State = ast.RefStateFound
	return &ast.ReferenceMaterialNode{Ref: ref}
}

Changes to web/adapter/webui/webui.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
10
11
12
13
14
15
16
17
18
19
20
21
22

23
24
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 webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"io"
	"log"
	"net/http"
	"sync"
	"time"

	"zettelstore.de/z/api"
	"zettelstore.de/c/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/template"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
)

// WebUI holds all data for delivering the web ui.
type WebUI struct {
	debug    bool
	ab       server.AuthBuilder
	authz    auth.AuthzManager
	rtConfig config.Config
	token    auth.TokenManager
	box      webuiBox
	policy   auth.Policy

71
72
73
74
75
76
77

78
79
80
81
82
83
84
85
86
87


88
89
90
91
92
93
94
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87


88
89
90
91
92
93
94
95
96







+








-
-
+
+







}

// 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 {
	loginoutBase := ab.NewURLBuilder('i')
	wui := &WebUI{
		debug:    kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration),
		cssBaseURL:    ab.NewURLBuilder('z').SetZid(id.BaseCSSZid).String(),
		cssUserURL:    ab.NewURLBuilder('z').SetZid(id.UserCSSZid).String(),
		cssBaseURL:    ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(),
		cssUserURL:    ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(),
		homeURL:       ab.NewURLBuilder('/').String(),
		listZettelURL: ab.NewURLBuilder('h').String(),
		listRolesURL:  ab.NewURLBuilder('h').AppendQuery("_l", "r").String(),
		listTagsURL:   ab.NewURLBuilder('h').AppendQuery("_l", "t").String(),
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendQuery("logout", "").String(),
190
191
192
193
194
195
196
197
198


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


199
200
201
202
203
204
205
206
207







-
-
+
+








func (wui *WebUI) makeBaseData(ctx context.Context, lang, title string, user *meta.Meta, data *baseData) {
	var userZettelURL string
	var userIdent string

	userIsValid := user != nil
	if userIsValid {
		userZettelURL = wui.NewURLBuilder('h').SetZid(user.Zid).String()
		userIdent = user.GetDefault(meta.KeyUserID, "")
		userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String()
		userIdent = user.GetDefault(api.KeyUserID, "")
	}
	newZettelLinks := wui.fetchNewTemplates(ctx, user)

	data.Lang = lang
	data.CSSBaseURL = wui.cssBaseURL
	data.CSSUserURL = wui.cssUserURL
	data.Title = title
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
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







-
-
+
+


-
-
+
+






-
+

-
-
-
-
+
+
+
+





-
+







	}
	menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}
	refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig))
	for _, ref := range refs {
		zid, err := id.Parse(ref.URL.Path)
		if err != nil {
		zid, err2 := id.Parse(ref.URL.Path)
		if err2 != nil {
			continue
		}
		m, err := wui.box.GetMeta(ctx, zid)
		if err != nil {
		m, err2 := wui.box.GetMeta(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, m) {
			continue
		}
		title := config.GetTitle(m, wui.rtConfig)
		astTitle := parser.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk)
		astTitle := parser.ParseMetadata(title)
		env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)}
		menuTitle, err := encodeInlines(astTitle, api.EncoderHTML, &env)
		if err != nil {
			menuTitle, err = encodeInlines(astTitle, api.EncoderText, nil)
			if err != nil {
		menuTitle, err2 := encodeInlines(astTitle, api.EncoderHTML, &env)
		if err2 != nil {
			menuTitle, err2 = encodeInlines(astTitle, api.EncoderText, nil)
			if err2 != nil {
				menuTitle = title
			}
		}
		result = append(result, simpleLink{
			Text: menuTitle,
			URL:  wui.NewURLBuilder('g').SetZid(m.Zid).String(),
			URL:  wui.NewURLBuilder('g').SetZid(api.ZettelID(m.Zid.String())).String(),
		})
	}
	return result
}

func (wui *WebUI) renderTemplate(
	ctx context.Context,
282
283
284
285
286
287
288
289

290
291
292
293
294
295
296
284
285
286
287
288
289
290

291
292
293
294
295
296
297
298







-
+







func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		log.Printf("%v: %v", text, err)
	}
	user := wui.getUser(ctx)
	var base baseData
	wui.makeBaseData(ctx, meta.ValueLangEN, "Error", user, &base)
	wui.makeBaseData(ctx, api.ValueLangEN, "Error", user, &base)
	wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct {
		ErrorTitle string
		ErrorText  string
	}{
		ErrorTitle: http.StatusText(code),
		ErrorText:  text,
	})
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












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







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

-
+

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













+



+
+
+
+
+
+
+
+
+
+
+
+
		if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil {
			wui.setToken(w, tok)
		}
	}
	var content bytes.Buffer
	err = t.Render(&content, data)
	if err == nil {
		wui.prepareAndWriteHeader(w, code)
		err = writeHTMLStart(w, base.Lang)
		if err == nil {
		base.Content = content.String()
			base.Content = content.String()
		w.Header().Set(api.HeaderContentType, "text/html; charset=utf-8")
		w.WriteHeader(code)
		err = bt.Render(w, base)
	}
			err = bt.Render(w, base)
			if err == nil {
				err = wui.writeHTMLEnd(w)
			}
		}
	}
	if err != nil {
		log.Println("Unable to render template", err)
		log.Println("Unable to write HTML via template", err)
	}
}

func writeHTMLStart(w http.ResponseWriter, lang string) error {
	_, err := io.WriteString(w, "<!DOCTYPE html>\n<html")
	if err != nil {
		return err
	}
	if lang != "" {
		_, err = io.WriteString(w, " lang=\"")
		if err == nil {
			_, err = io.WriteString(w, lang)
		}
		if err == nil {
			_, err = io.WriteString(w, "\">\n<head>\n")
		}
	} else {
		_, err = io.WriteString(w, ">\n<head>\n")
	}
	return err
}

func (wui *WebUI) writeHTMLEnd(w http.ResponseWriter) error {
	if wui.debug {
		_, err := io.WriteString(w, "<div><b>WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!</b></div>\n")
		if err != nil {
			return err
		}
	}
	_, err := io.WriteString(w, "</body>\n</html>")
	return 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) clearToken(ctx context.Context, w http.ResponseWriter) context.Context {
	return wui.ab.ClearToken(ctx, w)
}

func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) {
	wui.ab.SetToken(w, token, wui.tokenLifetime)
}

func (wui *WebUI) prepareAndWriteHeader(w http.ResponseWriter, statusCode int) {
	h := adapter.PrepareHeader(w, "text/html; charset=utf-8")
	h.Set("Content-Security-Policy", "default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'")
	h.Set("Permissions-Policy", "payment=(), interest-cohort=()")
	h.Set("Referrer-Policy", "no-referrer")
	h.Set("X-Content-Type-Options", "nosniff")
	if !wui.debug {
		h.Set("X-Frame-Options", "sameorigin")
	}
	w.WriteHeader(statusCode)
}

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
26







-
+







package impl

import (
	"context"
	"net/http"
	"time"

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

type myServer struct {
	server           httpServer
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
87
88
89
90
91
92
93

94
95
96
97
98
99
100
101







-
+







	if w != nil {
		srv.SetToken(w, nil, 0)
	}
	return updateContext(ctx, nil, nil)
}

// GetAuthData returns the full authentication data from the context.
func (srv *myServer) GetAuthData(ctx context.Context) *server.AuthData {
func (*myServer) GetAuthData(ctx context.Context) *server.AuthData {
	data, ok := ctx.Value(ctxKeySession).(*server.AuthData)
	if ok {
		return data
	}
	return nil
}

Changes to web/server/impl/router.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
26







-
+







package impl

import (
	"net/http"
	"regexp"
	"strings"

	"zettelstore.de/z/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/web/server"
)

type (
	methodHandler [server.MethodLAST]http.Handler
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







-
+







	mh := table[key]
	if mh == nil {
		mh = new(methodHandler)
		table[key] = mh
	}
	mh[method] = handler
	if method == server.MethodGet {
		if handler := mh[server.MethodHead]; handler == nil {
		if prevHandler := mh[server.MethodHead]; prevHandler == nil {
			mh[server.MethodHead] = handler
		}
	}
}

// addListRoute adds a route for the given key and HTTP method to work with a list.
func (rt *httpRouter) addListRoute(key byte, method server.Method, handler http.Handler) {

Changes to web/server/server.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
26







-
+







package server

import (
	"context"
	"net/http"
	"time"

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

// UserRetriever allows to retrieve user data based on a given zettel identifier.
type UserRetriever interface {
	GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error)

Changes to www/changes.wiki.

1
2
3
4

































































5
6
7
8
9
10
11
1
2


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


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







<title>Change Log</title>

<a name="0_0_16"></a>
<h2>Changes for Version 0.0.16 (pending)</h2>
<a name="0_2_0"></a>
<h2>Changes for Version 0.2.0 (pending)</h2>

<a name="0_1"></a><a name="0_1_0"></a>
<h2>Changes for Version 0.1 (2021-11-11)</h2>
  *  v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when
     more than one comparison is negated.
  *  v0.1.1 (2021-11-12) updates the documentation, mostly related to the
     deprecation of the <tt>//</tt> markup.
  *  Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup
     (emphasize, insert) is still allowed, but got a different syntax. The new
     syntax for <ins>inserted text</ins> is <tt>&gt;&gt;inserted&gt;&gt;</tt>,
     while its previous syntax now denotes <em>emphasized text</em>:
     <tt>__emphasized__</tt>. The previous syntax for emphasized text is now
     deprecated: <tt>//deprecated emphasized//</tt>. Starting with
     Version&nbsp;0.2.0, the deprecated syntax will not be supported. The
     reason is the collision with URLs that also contain the characters
     <tt>//</tt>. The ZMK encoding of a zettel may help with the transition
     (<tt>/v/{ZettelID}?_part=zettel&amp;_enc=zmk</tt>, on the Info page of
     each zettel in the WebUI). Additionally, all deprecated uses of
     <tt>//</tt> will be rendered with a dashed box within the WebUI.
     (breaking: Zettelmarkup).
  *  API client software is now a [https://zettelstore.de/client/|separate]
     project.
     (breaking)
  *  Initial support for HTTP security headers (Content-Security-Policy,
     Permissions-Policy, Referrer-Policy, X-Content-Type-Options,
     X-Frame-Options). Header values are currently some constant values.
     (possibly breaking: api, webui)
  *  Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup
     (strong, delete) is still allowed and replaces the visual elements
     syntactically. The visual appearance should not change (depends on your
     changes / additions to CSS zettel).
     (possibly breaking: Zettelmarkup).
  *  Add API endpoint <tt>POST /v</tt> to retrieve HTMl and text encoded
     strings from given ZettelMarkup encoded values. This will be used to
     render a HTML page from a given zettel: in many cases the title of
     a zettel must be treated separately.
     (minor: api)
  *  Add API endpoint <tt>/m</tt> to retrieve only the metadata of a zettel.
     (minor: api)
  *  New metadata value <tt>content-tags</tt> contains the tags that were given
     in the zettel content. To put it simply, <tt>all-tags</tt> = <tt>tags</tt>
     + <tt>content-tags</tt>.
     (minor)
  *  Calculating the context of a zettel stops at the home zettel.
     (minor: api, webui)
  *  When renaming or deleting a zettel, a warning will be given, if other
     zettel references the given zettel, or when &ldquo;deleting&rdquo; will
     uncover zettel in overlay box.
     (minor: webui)
  *  Fix: do not allow control characters in JSON-based creating/updating API.
     Otherwise, the created / updated zettel might not be parseable by the
     software (but still by a human). In certain cases, even the WebUI might be
     affected.
     (minor: api, webui)
  *  Fix: when a very long word (longer than width of browser window) is given,
     still allow to scroll horizontally.
     (minor: webui)
  *  Separate repository for [https://zettelstore.de/contrib/|contributed]
     software. First entry is a software for creating a presentation by using
     zettel.
     (info)
  *  Many smaller bug fixes and inprovements, to the software and to the
     documentation.

<a name="0_0_15"></a>
<h2>Changes for Version 0.0.15 (2021-09-17)</h2>
  *  Move again endpoint characters for authentication to make room for future
     features. WebUI authentication moves from <tt>/a</tt> to <tt>/i</tt>
     (login) and <tt>/i?logout</tt> (logout). API authentication moves from
     <tt>/v</tt> to </tt>/a</tt>. JSON-based basic zettel handling moves from

Changes to www/download.wiki.

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

13
14
15
16
17
18





19
20
21
22
23
24

25
26
1
2
3
4
5
6
7
8
9
10
11

12
13





14
15
16
17
18
19
20
21
22
23

24
25
26











-
+

-
-
-
-
-
+
+
+
+
+





-
+


<title>Download</title>
<h1>Download of Zettelstore Software</h1>
<h2>Foreword</h2>
  *  Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later.
  *  The software is provided as-is.
  *  There is no guarantee that it will not damage your system.
  *  However, it is in use by the main developer since March 2020 without any damage.
  *  It may be useful for you. It is useful for me.
  *  Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

<h2>ZIP-ped Executables</h2>
Build: <code>v0.0.15</code> (2021-09-17).
Build: <code>v0.1</code> (2021-11-11).

  *  [/uv/zettelstore-0.0.15-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.0.15-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.0.15-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.0.15-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.0.15-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/uv/zettelstore-0.1.2-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.1.2-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.1.2-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.1.2-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.1.2-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.15.zip|here]. Just unzip the contained files and put them into
[/uv/manual-0.1.2.zip|here]. Just unzip the contained files and put them into
your zettel folder or configure a file box to read the zettel directly from the
ZIP file.

Changes to www/index.wiki.

1
2
3
4
5
6
7
8
9
10





11
12
13



14
15
16






17
18
19
20

21
22
23
24
25
26





27
28
29
30
31
32
33
34
35
36
37
1
2
3
4
5





6
7
8
9
10
11


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

27
28





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





-
-
-
-
-
+
+
+
+
+

-
-
+
+
+



+
+
+
+
+
+



-
+

-
-
-
-
-
+
+
+
+
+











<title>Home</title>

<b>Zettelstore</b> is a software that collects and relates your notes
(&ldquo;zettel&rdquo;) to represent and enhance your knowledge. It helps with
many tasks of personal knowledge management by explicitly supporting the
[https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The
method is based on creating many individual notes, each with one idea or
information, that are related to each other. Since knowledge is typically build
up gradually, one major focus is a long-term store of these notes, hence the
name &ldquo;Zettelstore&rdquo;.
[https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The method is
based on creating many individual notes, each with one idea or information,
that are related to each other. Since knowledge is typically build up
gradually, one major focus is a long-term store of these notes, hence the name
&ldquo;Zettelstore&rdquo;.

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

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

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

[https://twitter.com/zettelstore|Stay tuned]&hellip;
<hr>
<h3>Latest Release: 0.0.15 (2021-09-17)</h3>
<h3>Latest Release: 0.1.2 (2021-11-18)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_0_15|Change summary]
  *  [/timeline?p=version-0.0.15&bt=version-0.0.14&y=ci|Check-ins for version 0.0.15],
     [/vdiff?to=version-0.0.15&from=version-0.0.14|content diff]
  *  [/timeline?df=version-0.0.15&y=ci|Check-ins derived from the 0.0.15 release],
     [/vdiff?from=version-0.0.15&to=trunk|content diff]
  *  [./changes.wiki#0_1|Change summary]
  *  [/timeline?p=v0.1.2&bt=v0.0.15&y=ci|Check-ins for version 0.1.2],
     [/vdiff?to=v0.1.2&from=v0.0.15|content diff]
  *  [/timeline?df=v0.1&y=ci|Check-ins derived from the 0.1 release],
     [/vdiff?from=v0.1&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;).