Zettelstore

Check-in Differences
Login

Check-in Differences

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

Difference From v0.17.0 To trunk

2024-12-23
21:06
Fix visibility of zettel "Lists Menu" ... (Leaf check-in: b4ca73f9d3 user: t73fde tags: trunk)
20:53
Refactor reverse runing for loop by using slices.Backward ... (check-in: c5021e3402 user: t73fde tags: trunk)
2024-03-06
15:02
Increase version to 0.18.0-dev to begin next development cycle
... (check-in: 51c141a192 user: stern tags: trunk)
2024-03-04
17:08
Version 0.17.0 ... (check-in: c863ee5f61 user: stern tags: trunk, release, v0.17.0)
13:47
Adapt to sx changes; add SPDX license identifiers ... (check-in: 5485ba3ce3 user: stern tags: trunk)

Changes to README.md.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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).







|
|







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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://t73f.de/r/zsc) provides client software to access
Zettelstore via its API more easily, [Zettelstore
Contrib](https://zettelstore.de/contrib) contains contributed software, which
often connects to Zettelstore via its API. Some of the software packages may be
experimental.

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

Changes to VERSION.

1
0.17.0
|
1
0.20.0-dev

Changes to ast/block.go.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

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

// Definition of Block nodes.

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

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







|







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

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

// Definition of Block nodes.

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

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

Changes to ast/inline.go.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

import (
	"unicode/utf8"

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

// Definitions of inline nodes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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








<
|
<









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



















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







10
11
12
13
14
15
16

17

18
19
20
21
22
23
24
25
26













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

















46
47
48
49
50
51
52
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

import (

	"t73f.de/r/zsc/attrs"

)

// Definitions of inline nodes.

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

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














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

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

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

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

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


















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

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

Changes to ast/ref.go.

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

package ast

import (
	"net/url"
	"strings"

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

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

// ParseReference parses a string and returns a reference.







|







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

package ast

import (
	"net/url"
	"strings"

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

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

// ParseReference parses a string and returns a reference.

Changes to ast/walk_test.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
//-----------------------------------------------------------------------------

package ast_test

import (
	"testing"

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

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







|






|


|






|




|





|








|

|


|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 ast_test

import (
	"testing"

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

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

Changes to auth/auth.go.

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

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

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

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

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

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







<
<
<






91
92
93
94
95
96
97



98
99
100
101
102
103

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

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




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

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

Changes to auth/impl/digest.go.

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

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

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

var encoding = base64.RawURLEncoding

const digestAlg = crypto.SHA384

func sign(claim sx.Object, secret []byte) ([]byte, error) {







|
|







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

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

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

var encoding = base64.RawURLEncoding

const digestAlg = crypto.SHA384

func sign(claim sx.Object, secret []byte) ([]byte, error) {

Changes to auth/impl/impl.go.

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

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"







|
|
|







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

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

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
	if !ok || subject == "" {
		return nil, ErrNoIdent
	}

	now := time.Now().Round(time.Second)
	sClaim := sx.MakeList(
		sx.Int64(kind),
		sx.String(subject),
		sx.Int64(now.Unix()),
		sx.Int64(now.Add(d).Unix()),
		sx.Int64(ident.Zid),
	)
	return sign(sClaim, a.secret)
}








|







88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
	if !ok || subject == "" {
		return nil, ErrNoIdent
	}

	now := time.Now().Round(time.Second)
	sClaim := sx.MakeList(
		sx.Int64(kind),
		sx.MakeString(subject),
		sx.Int64(now.Unix()),
		sx.Int64(now.Add(d).Unix()),
		sx.Int64(ident.Zid),
	)
	return sign(sClaim, a.secret)
}

121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
	vals, err := sexp.ParseList(obj, "isiii")
	if err != nil {
		return ErrMalformedToken
	}
	if auth.TokenKind(vals[0].(sx.Int64)) != k {
		return ErrOtherKind
	}
	ident := vals[1].(sx.String)
	if ident == "" {
		return ErrNoIdent
	}
	issued := time.Unix(int64(vals[2].(sx.Int64)), 0)
	expires := time.Unix(int64(vals[3].(sx.Int64)), 0)
	now := time.Now().Round(time.Second)
	if expires.Before(now) {







|







121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
	vals, err := sexp.ParseList(obj, "isiii")
	if err != nil {
		return ErrMalformedToken
	}
	if auth.TokenKind(vals[0].(sx.Int64)) != k {
		return ErrOtherKind
	}
	ident := vals[1].(sx.String).GetValue()
	if ident == "" {
		return ErrNoIdent
	}
	issued := time.Unix(int64(vals[2].(sx.Int64)), 0)
	expires := time.Unix(int64(vals[3].(sx.Int64)), 0)
	now := time.Now().Round(time.Second)
	if expires.Before(now) {

Changes to auth/policy/anon.go.

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	return ap.pre.CanRead(user, m) && ap.checkVisibility(m)
}

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

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

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

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







<
<
<
<







32
33
34
35
36
37
38




39
40
41
42
43
44
45
	return ap.pre.CanRead(user, m) && ap.checkVisibility(m)
}

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





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

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

Changes to auth/policy/box.go.

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
	return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
}

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

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

func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	m, err := pp.box.GetMeta(ctx, zid)
	if err != nil {
		return nil, err







|







74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
	return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
}

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

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

func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	m, err := pp.box.GetMeta(ctx, zid)
	if err != nil {
		return nil, err
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
	}
	if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) {
		return pp.box.UpdateZettel(ctx, zettel)
	}
	return box.NewErrNotAllowed("Write", user, zid)
}

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

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

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

func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {







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







118
119
120
121
122
123
124
















125
126
127
128
129
130
131
	}
	if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) {
		return pp.box.UpdateZettel(ctx, zettel)
	}
	return box.NewErrNotAllowed("Write", user, zid)
}

















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

func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {

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

package policy

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

type defaultPolicy struct {
	manager auth.AuthzManager
}

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

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

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







|













<







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

31
32
33
34
35
36
37
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

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

type defaultPolicy struct {
	manager auth.AuthzManager
}

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

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

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

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

Changes to auth/policy/owner.go.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

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

type ownerPolicy struct {
	manager    auth.AuthzManager







|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

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

type ownerPolicy struct {
	manager    auth.AuthzManager
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
	switch userRole := o.manager.GetUserRole(user); userRole {
	case meta.UserRoleReader, meta.UserRoleCreator:
		return false
	}
	return o.userCanCreate(user, newMeta)
}

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

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







<
<
<
<
<
<
<
<
<
<







111
112
113
114
115
116
117










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











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

Changes to auth/policy/policy.go.

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

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

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

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

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







<
<
<
<







56
57
58
59
60
61
62




63
64
65
66
67
68
69
}

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





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

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

Changes to auth/policy/policy_test.go.

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

package policy

import (
	"fmt"
	"testing"

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

func TestPolicies(t *testing.T) {
	t.Parallel()







|







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

package policy

import (
	"fmt"
	"testing"

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

func TestPolicies(t *testing.T) {
	t.Parallel()
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
		)
		name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v/simple=%v",
			ts.readonly, ts.withAuth, ts.expert, ts.simple)
		t.Run(name, func(tt *testing.T) {
			testCreate(tt, pol, ts.withAuth, ts.readonly)
			testRead(tt, pol, ts.withAuth, ts.expert)
			testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple)
		})
	}
}

type testAuthzManager struct {







<







55
56
57
58
59
60
61

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

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

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

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

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







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







390
391
392
393
394
395
396
























































































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
























































































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

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

Changes to auth/policy/readonly.go.

16
17
18
19
20
21
22
23
24
25
import "zettelstore.de/z/zettel/meta"

type roPolicy struct{}

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







<


16
17
18
19
20
21
22

23
24
import "zettelstore.de/z/zettel/meta"

type roPolicy struct{}

func (*roPolicy) CanCreate(_, _ *meta.Meta) bool   { return false }
func (*roPolicy) CanRead(_, _ *meta.Meta) bool     { return true }
func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false }

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

Changes to box/box.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import (
	"context"
	"errors"
	"fmt"
	"io"
	"time"

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

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

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

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

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

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

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








|















<
<
<
<
<
<







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






40
41
42
43
44
45
46
import (
	"context"
	"errors"
	"fmt"
	"io"
	"time"

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

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

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







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

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

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

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

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

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

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







|







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

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

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

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

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

216
217
218
219
220
221
222
223
224
225
226



227
228
229
230
231
232
233
type UpdateReason uint8

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

)

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

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




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







|
>











>
>
>







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
type UpdateReason uint8

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

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

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

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

// Subject is a box that notifies observers about changes.
type Subject interface {
	// RegisterObserver registers an observer that will be notified
	// if one or all zettel are found to be changed.
	RegisterObserver(UpdateFunc)
}
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
	return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
}

type ctxNoEnrichType struct{}

var ctxNoEnrichKey ctxNoEnrichType

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

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







|
|

|







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

type ctxNoEnrichType struct{}

var ctxNoEnrichKey ctxNoEnrichType

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

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

Changes to box/compbox/compbox.go.

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






55


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Package compbox provides zettel that have computed content.
package compbox

import (
	"context"
	"net/url"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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






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


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

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

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

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

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







|













|













|

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






|













|







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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 compbox provides zettel that have computed content.
package compbox

import (
	"context"
	"net/url"

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

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

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

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

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

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

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

func (cb *compBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	if gen, ok := myZettel[zid]; ok && gen.meta != nil {
		if m := gen.meta(zid); m != nil {
			updateMeta(m)
			if genContent := gen.content; genContent != nil {
				cb.log.Trace().Msg("GetZettel/Content")
				return zettel.Zettel{
					Meta:    m,
					Content: zettel.NewContent(genContent(ctx, cb)),
				}, nil
			}
			cb.log.Trace().Msg("GetZettel/NoContent")
			return zettel.Zettel{Meta: m}, nil
		}
	}
	err := box.ErrZettelNotFound{Zid: zid}
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164






165
166
167
168
169
170



171
172
173
174
175
176
				handle(m)
			}
		}
	}
	return nil
}

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

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

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

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

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







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



	m.Set(api.KeyLang, api.ValueLangEN)
	m.Set(api.KeyReadOnly, api.ValueTrue)
	if _, ok := m.Get(api.KeyVisibility); !ok {
		m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	}
}







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

















>
>
>
>
>
>






>
>
>






134
135
136
137
138
139
140















141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
				handle(m)
			}
		}
	}
	return nil
}
















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

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

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

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

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

Changes to box/compbox/config.go.

11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"


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

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

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







>

<
<








<
|
<
<
<


|







11
12
13
14
15
16
17
18
19


20
21
22
23
24
25
26
27

28



29
30
31
32
33
34
35
36
37
38
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"context"



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

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

	return getTitledMeta(zid, "Zettelstore Startup Configuration")



}

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

Changes to box/compbox/keys.go.

11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"

	"fmt"

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

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

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







>


|






<
|





|







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

28
29
30
31
32
33
34
35
36
37
38
39
40
41
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"context"
	"fmt"

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

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

	m := getTitledMeta(zid, "Zettelstore Supported Metadata Keys")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}

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

Changes to box/compbox/log.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
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"


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

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

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







>

|






<
|

<




|







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

27
28

29
30
31
32
33
34
35
36
37
38
39
40
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"context"

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

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

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

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

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

Changes to box/compbox/manager.go.

11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"

	"fmt"

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

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

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







>


<






<
|
<
<


|







11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26

27


28
29
30
31
32
33
34
35
36
37
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"context"
	"fmt"


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

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

	return getTitledMeta(zid, "Zettelstore Box Manager")


}

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

Added box/compbox/memory.go.















































































































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

package compbox

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

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

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

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

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

Changes to box/compbox/parser.go.

11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"

	"fmt"
	"sort"
	"strings"

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

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

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







>

|


|







<
|





|



|






|






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

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

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

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

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

	m := getTitledMeta(zid, "Zettelstore Supported Parser")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}

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

Added box/compbox/sx.go.







































































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

package compbox

import (
	"bytes"
	"context"
	"fmt"

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

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

func genSxC(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Name|=Value>\n")
	fmt.Fprintf(&buf, "|Symbols|%d\n", sx.MakeSymbol("NIL").Factory().Size())
	return buf.Bytes()
}

Changes to box/compbox/version.go.

10
11
12
13
14
15
16

17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (

	"zettelstore.de/client.fossil/api"

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

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

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

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

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







>
|
>





<
<
<
<
<
<
<

|




|




|
<
<

|




|
<
<

|







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







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


37
38
39
40
41
42
43


44
45
46
47
48
49
50
51
52
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"context"

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








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

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


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

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


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

Changes to box/constbox/base.css.

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

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







<







12
13
14
15
16
17
18

19
20
21
22
23
24
25
 *-----------------------------------------------------------------------------
 */

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

    font-family: serif;
    scroll-behavior: smooth;
    height: 100%;
  }
  body {
    margin: 0;
    min-height: 100vh;
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

120
121
122
123
124
125
126
127
128
129
130
131
132
133
  .zs-dropdown:hover > .zs-dropdown-content { display: block }
  main { padding: 0 1rem }
  article > * + * { margin-top: .5rem }
  article header {
    padding: 0;
    margin: 0;
  }
  h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal }
  h1 { font-size:1.5rem;  margin:.65rem 0 }
  h2 { font-size:1.25rem; margin:.70rem 0 }
  h3 { font-size:1.15rem; margin:.75rem 0 }
  h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold }
  h5 { font-size:1.05rem; margin:.8rem 0 }
  h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter }
  p { margin: .5rem 0 0 0 }
  p.zs-meta-zettel { margin-top: .5rem; margin-left: 0.5rem }
  li,figure,figcaption,dl { margin: 0 }
  dt { margin: .5rem 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5rem 0 0 2rem }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: 0.5rem solid lightgray;
    padding-left: 1rem;
    margin-left: 1rem;
    margin-right: 2rem;
    font-style: italic;
  }
  blockquote p { margin-bottom: .5rem }
  blockquote cite { font-style: normal }
  table {
    border-collapse: collapse;
    border-spacing: 0;
    max-width: 100%;
  }

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







|
|
|
|
|
|
|
|
|

|

|


|
|
|
|
<

|
<





>
|
|
<
<
<
|
<







84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

110
111

112
113
114
115
116
117
118
119



120

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

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

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



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

  main form {
    padding: 0 .5em;
    margin: .5em 0 0 0;
  }
  main form:after {
    content: ".";
    display: block;
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
  input.zs-secondary { float:left }
  input.zs-upload {
    padding-left: 1em;
    padding-right: 1em;
  }
  a:not([class]) { text-decoration-skip-ink: auto }
  a.broken { text-decoration: line-through }
  a.external::after { content: "âžš"; display: inline-block }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1rem .2rem;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25rem;
  }
  pre {
    padding: .5rem .7rem;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5rem;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5rem .7rem;
    max-width: 100%;
    border-radius: .5rem;
    border: 1px solid black;
  }
  div.zs-indication p:first-child { margin-top: 0 }
  span.zs-indication {
    border: 1px solid black;
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 95%;
  }
  .zs-info {
    background-color: lightblue;
    padding: .5rem 1rem;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5rem 1rem;
  }
  .zs-error {
    background-color: lightpink;
    border-style: none !important;
    font-weight: bold;
  }
  td.left { text-align:left }
  td.center { text-align:center }
  td.right { text-align:right }
  .zs-font-size-0 { font-size:75% }
  .zs-font-size-1 { font-size:83% }
  .zs-font-size-2 { font-size:100% }
  .zs-font-size-3 { font-size:117% }
  .zs-font-size-4 { font-size:150% }
  .zs-font-size-5 { font-size:200% }
  .zs-deprecated { border-style: dashed; padding: .2rem }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1rem;
  }
  .zs-meta a { color:#444 }
  h1+.zs-meta { margin-top:-1rem }
  nav > details { margin-top:1rem }
  details > summary {
    width: 100%;
    background-color: #eee;
    font-family:sans-serif;
  }
  details > ul {
    margin-top:0;
    padding-left:2rem;
    background-color: #eee;
  }
  footer { padding: 0 1rem }
  @media (prefers-reduced-motion: reduce) {
    * {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }







|



|








|


|


|



|









|

|





|
|




|



|






|
|
|






|



|


|
|







|


|








149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
  input.zs-secondary { float:left }
  input.zs-upload {
    padding-left: 1em;
    padding-right: 1em;
  }
  a:not([class]) { text-decoration-skip-ink: auto }
  a.broken { text-decoration: line-through }
  a[rel~="external"]::after { content: "âžš"; display: inline-block }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5em;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1em .2em;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25em;
  }
  pre {
    padding: .5em .7em;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5em;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5em .7em;
    max-width: 100%;
    border-radius: .5em;
    border: 1px solid black;
  }
  div.zs-indication p:first-child { margin-top: 0 }
  span.zs-indication {
    border: 1px solid black;
    border-radius: .25em;
    padding: .1rem .2em;
    font-size: 95%;
  }
  .zs-info {
    background-color: lightblue;
    padding: .5em 1em;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5em 1em;
  }
  .zs-error {
    background-color: lightpink;
    border-style: none !important;
    font-weight: bold;
  }
  td.left, th.left { text-align:left }
  td.center, th.center { text-align:center }
  td.right, th.right { text-align:right }
  .zs-font-size-0 { font-size:75% }
  .zs-font-size-1 { font-size:83% }
  .zs-font-size-2 { font-size:100% }
  .zs-font-size-3 { font-size:117% }
  .zs-font-size-4 { font-size:150% }
  .zs-font-size-5 { font-size:200% }
  .zs-deprecated { border-style: dashed; padding: .2em }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1em;
  }
  .zs-meta a { color:#444 }
  h1+.zs-meta { margin-top:-1em }
  nav > details { margin-top:1em }
  details > summary {
    width: 100%;
    background-color: #eee;
    font-family:sans-serif;
  }
  details > ul {
    margin-top:0;
    padding-left:2em;
    background-color: #eee;
  }
  footer { padding: 0 1em }
  @media (prefers-reduced-motion: reduce) {
    * {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }

Changes to box/constbox/constbox.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
package constbox

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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







|













|







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

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

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

func init() {
	manager.Register(
		" const",
		func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &constBox{
				log: kernel.Main.GetLogger(kernel.BoxService).Clone().
					Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(),
				number:   cdata.Number,
				zettel:   constZettelMap,
				enricher: cdata.Enricher,
			}, nil
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

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

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

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

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







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







93
94
95
96
97
98
99















100
101
102
103
104
105
106
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}
















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

func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {
	if _, ok := cb.zettel[zid]; ok {
		err = box.ErrReadOnly
	} else {
		err = box.ErrZettelNotFound{Zid: zid}
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20230601163100",
		},
		zettel.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,







|







153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20240418095500",
		},
		zettel.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
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
		zettel.NewContent(contentLoginSxn)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20240219145100",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentZettelSxn)},
	id.InfoTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Info HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.FormTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},
	id.RenameTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Rename Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentRenameSxn)},
	id.DeleteTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,







|









|













<
<
<
<
<
<
<
<
<
<






|







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
		zettel.NewContent(contentLoginSxn)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20241127170400",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentZettelSxn)},
	id.InfoTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Info HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20241127170500",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.FormTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},










	id.DeleteTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20241127170530",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
		zettel.NewContent(contentStartCodeSxn)},
	id.BaseSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Base Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",
			api.KeyModified:   "20240219144600",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnPrelude),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.PreludeSxnZid: {
		constHeader{







|







253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
		zettel.NewContent(contentStartCodeSxn)},
	id.BaseSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Base Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",
			api.KeyModified:   "20241118173500",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnPrelude),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.PreludeSxnZid: {
		constHeader{
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
		zettel.NewContent(contentPreludeSxn)},
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20231129112800",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,







|







276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
		zettel.NewContent(contentPreludeSxn)},
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240827143500",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
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
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyCreated:    "20210504175807",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentEmoji)},










	id.TOCNewTemplateZid: {
		constHeader{
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",
			api.KeyModified:   "20231129111800",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:                 "New Zettel",
			api.KeyRole:                  api.ValueRoleConfiguration,
			api.KeySyntax:                meta.SyntaxZmk,
			api.KeyCreated:               "20201028185209",
			api.KeyModified:              "20230929132900",







>
>
>
>
>
>
>
>
>
>










|







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
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyCreated:    "20210504175807",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentEmoji)},
	id.TOCListsMenuZid: {
		constHeader{
			api.KeyTitle:      "Lists Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20241223205400",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentMenuListsZettel)},
	id.TOCNewTemplateZid: {
		constHeader{
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",
			api.KeyModified:   "20231129111800",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentMenuNewZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:                 "New Zettel",
			api.KeyRole:                  api.ValueRoleConfiguration,
			api.KeySyntax:                meta.SyntaxZmk,
			api.KeyCreated:               "20201028185209",
			api.KeyModified:              "20230929132900",
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
		},
		zettel.NewContent(contentRoleZettel)},
	id.MustParse(api.ZidRoleConfigurationZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleConfiguration,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162800",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleConfiguration)},
	id.MustParse(api.ZidRoleRoleZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleRole,







|







380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
		},
		zettel.NewContent(contentRoleZettel)},
	id.MustParse(api.ZidRoleConfigurationZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleConfiguration,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20241213103100",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleConfiguration)},
	id.MustParse(api.ZidRoleRoleZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleRole,
420
421
422
423
424
425
426










427
428
429
430
431
432
433

434
435
436
437
438
439
440
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162000",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleTag)},










	id.DefaultHomeZid: {
		constHeader{
			api.KeyTitle:   "Home",
			api.KeyRole:    api.ValueRoleZettel,
			api.KeySyntax:  meta.SyntaxZmk,
			api.KeyLang:    api.ValueLangEN,
			api.KeyCreated: "20210210190757",

		},
		zettel.NewContent(contentHomeZettel)},
}

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








>
>
>
>
>
>
>
>
>
>


|
|
|
|
|
>







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
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162000",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleTag)},
	id.MustParse(api.ZidAppDirectory): {
		constHeader{
			api.KeyTitle:      "Zettelstore Application Directory",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxNone,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20240703235900",
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(nil)},
	id.DefaultHomeZid: {
		constHeader{
			api.KeyTitle:    "Home",
			api.KeyRole:     api.ValueRoleZettel,
			api.KeySyntax:   meta.SyntaxZmk,
			api.KeyLang:     api.ValueLangEN,
			api.KeyCreated:  "20210210190757",
			api.KeyModified: "20241216105800",
		},
		zettel.NewContent(contentHomeZettel)},
}

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

455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471

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

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

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

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

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

//go:embed error.sxn







<
<
<







451
452
453
454
455
456
457



458
459
460
461
462
463
464

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

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




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

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

//go:embed error.sxn
482
483
484
485
486
487
488



489
490
491
492
493
494
495
496
497

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

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




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

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

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








>
>
>
|
|







475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493

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

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

//go:embed menu_lists.zettel
var contentMenuListsZettel []byte

//go:embed menu_new.zettel
var contentMenuNewZettel []byte

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

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

Changes to box/constbox/dependencies.zettel.

126
127
128
129
130
131
132
133
134
135
136
137
138


139


140
141
142
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

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

; URL & Source sx
: [[https://zettelstore.de/sx]]


; URL & Source zettelstore-client


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







|
|


|
|
>
>
|
>
>
|


126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

=== Sx, SxWebs, Webs, Zettelstore-Client
These are companion projects, written by the main developer of Zettelstore.
They are published under the same license, [[EUPL v1.2, or later|00000000000004]].

; URL & Source Sx
: [[https://t73f.de/r/sx]]
; URL & Source SxWebs
: [[https://t73f.de/r/sxwebs]]
; URL & Source Webs
: [[https://t73f.de/r/webs]]
; URL & Source Zettelstore-Client
: [[https://t73f.de/r/zsc]]
; License:
: European Union Public License, version 1.2 (EUPL v1.2), or later.

Changes to box/constbox/home.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=== Thank you for using Zettelstore!

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

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

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



|
|
|
|

<
|







1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
16
=== Thank you for using Zettelstore!

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


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

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

Changes to box/constbox/info.sxn.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      (@H " / ") (a (@ (href ,context-full-url)) "Full")
      ,@(if (bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      ,@(ROLE-DEFAULT-actions (current-binding))
      ,@(if (bound? 'reindex-url) `((@H " &#183; ") (a (@ (href ,reindex-url)) "Reindex")))
      ,@(if (bound? 'rename-url) `((@H " &#183; ") (a (@ (href ,rename-url)) "Rename")))
      ,@(if (bound? 'delete-url) `((@H " &#183; ") (a (@ (href ,delete-url)) "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
  (h2 "References")
  ,@(if local-links `((h3 "Local")    (ul ,@(map wui-valid-link local-links))))
  ,@(if query-links `((h3 "Queries")  (ul ,@(map wui-item-link query-links))))
  ,@(if ext-links   `((h3 "External") (ul ,@(map wui-item-popup-link ext-links))))
  (h3 "Unlinked")
  ,@unlinked-content
  (form
    (label (@ (for "phrase")) "Search Phrase")
    (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase)))







<






|







16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
34
35
36
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      (@H " / ") (a (@ (href ,context-full-url)) "Full")
      ,@(if (bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      ,@(ROLE-DEFAULT-actions (current-binding))
      ,@(if (bound? 'reindex-url) `((@H " &#183; ") (a (@ (href ,reindex-url)) "Reindex")))

      ,@(if (bound? 'delete-url) `((@H " &#183; ") (a (@ (href ,delete-url)) "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
  (h2 "References")
  ,@(if local-links `((h3 "Local")    (ul ,@(map wui-local-link local-links)))) 
  ,@(if query-links `((h3 "Queries")  (ul ,@(map wui-item-link query-links))))
  ,@(if ext-links   `((h3 "External") (ul ,@(map wui-item-popup-link ext-links))))
  (h3 "Unlinked")
  ,@unlinked-content
  (form
    (label (@ (for "phrase")) "Search Phrase")
    (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase)))

Added box/constbox/menu_lists.zettel.















>
>
>
>
>
>
>
1
2
3
4
5
6
7
This zettel lists all entries of the ""Lists"" menu.

* [[List Zettel|query:]]
* [[List Roles|query:|role]]
* [[List Tags|query:|tags]]

An additional ""Refresh"" menu item is automatically added if appropriate.

Added box/constbox/menu_new.zettel.













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

Deleted box/constbox/newtoc.zettel.

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












Changes to box/constbox/prelude.sxn.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

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

;; Constants NIL and T
(defconst NIL ())
(defconst T   'T)

;; defunconst macro to define functions that are bound as a constant.
;;
;; (defunconst NAME ARGS EXPR ...)
(defmacro defunconst (name args . body)
    `(begin (defun ,name ,args ,@body) (defconst ,name ,name)))

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

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

;; let macro
;;
;; (let (BINDING ...) EXPR ...), where BINDING is a list of two elements
;;   (SYMBOL EXPR)
(defmacro let (bindings . body)
    `((lambda ,(map car bindings) ,@body) ,@(map cadr bindings)))

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

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

;; and macro
;;
;; (and EXPR ...)
(defmacro and args
    (cond ((null? args)       T)







|
<
<
<
<
<
<
<
<
<
<







<
<
<
<
<
<
<





|
|
|
<










|

|







9
10
11
12
13
14
15
16










17
18
19
20
21
22
23







24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

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











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

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








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


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

;; and macro
;;
;; (and EXPR ...)
(defmacro and args
    (cond ((null? args)       T)

Deleted box/constbox/rename.sxn.

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

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




















































































Changes to box/constbox/roleconfiguration.zettel.

1
2
3


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

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



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

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

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

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

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


|
>
>










|






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

Typically, there are some public zettel that show the license of this software, its dependencies.
There is some CSS code to make the default web user interface a litte bit nicer.
The default image to signal a broken image can be configured too.

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

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

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

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

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

Changes to box/constbox/wuicode.sxn.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

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

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

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

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

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

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

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

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

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

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

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

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

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

;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel
;; identifier. It is used in the base template to update the metadata of the







|



|


|
<
<
<
|
<


<
|



|



|



|
|


|



|





|



|



|







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



25

26
27

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

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

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

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

;; wui-local-link translates a local link into HTML.



(defun wui-local-link (l) `(li (a (@ (href ,l )) ,l)))


;; wui-link takes a link (title . url) and returns a HTML reference.

(defun wui-link (q) `(a (@ (href ,(cdr q))) ,(car q)))

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

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

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

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

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

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

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

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

;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel
;; identifier. It is used in the base template to update the metadata of the
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

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

;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag".
(defun ROLE-tag-actions (binding)







|
|







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

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

;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag".
(defun ROLE-tag-actions (binding)

Changes to box/constbox/zettel.sxn.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
          ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      ,@(ROLE-DEFAULT-actions (current-binding))
      ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
      ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
      ,@(if superior-refs `((br) "Superior: " ,superior-refs))
      ,@(ROLE-DEFAULT-heading (current-binding))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links subordinate-links back-links successor-links)
    `((nav
      ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
      ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links)))))
      ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
      ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links)))))
     ))
  )
)







|





|


|





22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
          ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      ,@(ROLE-DEFAULT-actions (current-binding))
      ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
      ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
      ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs))
      ,@(ROLE-DEFAULT-heading (current-binding))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links sequel-links back-links successor-links)
    `((nav
      ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
      ,@(if sequel-links `((details (@ (,sequel-open)) (summary "Sequel") (ul ,@(map wui-item-link sequel-links)))))
      ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
      ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links)))))
     ))
  )
)

Changes to box/dirbox/dirbox.go.

199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216

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

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

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







|
|
|
|







199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216

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

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

func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd {
	// Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
	sum := 2166136261 ^ uint32(zid)
	sum *= 16777619
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
	entry := notify.DirEntry{Zid: newZid}
	dp.updateEntryFromMetaContent(&entry, meta, zettel.Content)

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

func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {







|







240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
	entry := notify.DirEntry{Zid: newZid}
	dp.updateEntryFromMetaContent(&entry, meta, zettel.Content)

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

func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
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
		// Existing zettel, but new in this box.
		entry = &notify.DirEntry{Zid: zid}
	}
	dp.updateEntryFromMetaContent(entry, meta, zettel.Content)
	dp.dirSrv.UpdateDirEntry(entry)
	err := dp.srvSetZettel(ctx, entry, zettel)
	if err == nil {
		dp.notifyChanged(zid)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel")
	return err
}

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

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

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

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

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

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

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







|









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







312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328














































329
330
331
332
333
334
335
		// Existing zettel, but new in this box.
		entry = &notify.DirEntry{Zid: zid}
	}
	dp.updateEntryFromMetaContent(entry, meta, zettel.Content)
	dp.dirSrv.UpdateDirEntry(entry)
	err := dp.srvSetZettel(ctx, entry, zettel)
	if err == nil {
		dp.notifyChanged(zid, box.OnZettel)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel")
	return err
}

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















































func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	if dp.readonly {
		return false
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	return entry.IsValid()
}
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
	}
	err := dp.dirSrv.DeleteDirEntry(zid)
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {
		dp.notifyChanged(zid)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel")
	return err
}

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







|










345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
	}
	err := dp.dirSrv.DeleteDirEntry(zid)
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {
		dp.notifyChanged(zid, box.OnDelete)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel")
	return err
}

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

Changes to box/dirbox/service.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"time"

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







|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"time"

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/box/filebox"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
358
359
360
361
362
363
364
365






366
367
368
369
370
371
372
373
374
		m,
		entry.Zid,
		entry.ContentExt,
		entry.MetaName != "",
		entry.UselessFiles,
	)
}







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

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








>
>
>
>
>
>

|







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

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

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

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

Changes to box/filebox/filebox.go.

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

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

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








|







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

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

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

Changes to box/filebox/zipbox.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
import (
	"archive/zip"
	"context"
	"fmt"
	"io"
	"strings"

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

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

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







|














|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import (
	"archive/zip"
	"context"
	"fmt"
	"io"
	"strings"

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

func (zb *zipBox) Location() string {
	if strings.HasPrefix(zb.name, "/") {
		return "file://" + zb.name
	}
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}

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

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

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

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







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







168
169
170
171
172
173
174


















175
176
177
178
179
180
181
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}



















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

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

Changes to box/helper.go.

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// GetQueryBool is a helper function to extract bool values from a box URI.
func GetQueryBool(u *url.URL, key string) bool {
	_, ok := u.Query()[key]
	return ok
}

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







|


|



|

|
|

|
|



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// GetQueryBool is a helper function to extract bool values from a box URI.
func GetQueryBool(u *url.URL, key string) bool {
	_, ok := u.Query()[key]
	return ok
}

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

Changes to box/manager/anteroom.go.

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	arNothing arAction = iota
	arReload
	arZettel
)

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

type anteroomQueue struct {
	mx      sync.Mutex
	first   *anteroom







|







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	arNothing arAction = iota
	arReload
	arZettel
)

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

type anteroomQueue struct {
	mx      sync.Mutex
	first   *anteroom
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not put zettel in reload room
		}
		if _, ok := room.waiting[zid]; ok {
			// Zettel is already waiting. Nothing to do.
			return
		}
	}
	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
		room.waiting.Add(zid)
		room.curLoad++







|







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not put zettel in reload room
		}
		if room.waiting.Contains(zid) {
			// Zettel is already waiting. Nothing to do.
			return
		}
	}
	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
		room.waiting.Add(zid)
		room.curLoad++
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
func (ar *anteroomQueue) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true}
	ar.last = ar.first
}

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

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







|




|
|







84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
func (ar *anteroomQueue) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true}
	ar.last = ar.first
}

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

	if !allZids.IsEmpty() {
		ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: allZids.Length(), reload: true}
		if ar.first.next == nil {
			ar.last = ar.first
		}
	} else {
		ar.first = nil
		ar.last = nil
	}
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
	defer ar.mx.Unlock()
	first := ar.first
	if first != nil {
		if first.waiting == nil && first.reload {
			ar.removeFirst()
			return arReload, id.Invalid, false
		}
		for zid := range first.waiting {
			delete(first.waiting, zid)
			if len(first.waiting) == 0 {
				ar.removeFirst()
			}
			return arZettel, zid, first.reload
		}
		ar.removeFirst()
	}
	return arNothing, id.Invalid, false







|
|
<







120
121
122
123
124
125
126
127
128

129
130
131
132
133
134
135
	defer ar.mx.Unlock()
	first := ar.first
	if first != nil {
		if first.waiting == nil && first.reload {
			ar.removeFirst()
			return arReload, id.Invalid, false
		}
		if zid, found := first.waiting.Pop(); found {
			if first.waiting.IsEmpty() {

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

Changes to box/manager/box.go.

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123




124






125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

148
149
150

151
152
153
154
155

156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
		sb.WriteString(mgr.boxes[i].Location())
	}
	return sb.String()
}

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

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

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel")
	if mgr.State() != box.StartStateStarted {
		return zettel.Zettel{}, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()



	for i, p := range mgr.boxes {
		var errZNF box.ErrZettelNotFound
		if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) {
			if err == nil {
				mgr.Enrich(ctx, z.Meta, i+1)
			}
			return z, err
		}
	}
	return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
}

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

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZids")
	if mgr.State() != box.StartStateStarted {
		return nil, box.ErrStopped
	}
	result := id.Set{}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()




	for _, p := range mgr.boxes {






		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, func(id.Zid) bool { return true })
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

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


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

		return nil, box.ErrStopped
	}

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

		return nil, err
	}
	mgr.Enrich(ctx, m, 0)
	return m, nil
}

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

	compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq)
	if result := compSearch.Result(); result != nil {
		mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta")
		return result, nil
	}
	selected := map[id.Zid]*meta.Meta{}
	for _, term := range compSearch.Terms {
		rejected := id.Set{}
		handleMeta := func(m *meta.Meta) {
			zid := m.Zid
			if rejected.ContainsOrNil(zid) {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected")
				return
			}
			if _, ok := selected[zid]; ok {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected")
				return
			}







|











|

|
|




|
|

|









|
|



>
>
>















|
|














|

|
|

<


>
>
>
>

>
>
>
>
>
>
|







|

|












>


<
>
|




>












|
|











|


|







40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
		sb.WriteString(mgr.boxes[i].Location())
	}
	return sb.String()
}

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

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

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

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

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (*id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZids")
	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}

	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.fetchZids(ctx)
}
func (mgr *Manager) fetchZids(ctx context.Context) (*id.Set, error) {
	numZettel := 0
	for _, p := range mgr.boxes {
		var mbstats box.ManagedBoxStats
		p.ReadStats(&mbstats)
		numZettel += mbstats.Zettel
	}
	result := id.NewSetCap(numZettel)
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, query.AlwaysIncluded)
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

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

// GetMeta returns just the metadata of the zettel with the given identifier.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta")

	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}

	m, err := mgr.idxStore.GetMeta(ctx, zid)
	if err != nil {
		// TODO: Call GetZettel and return just metadata, in case the index is not complete.
		return nil, err
	}
	mgr.Enrich(ctx, m, 0)
	return m, nil
}

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

	compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq)
	if result := compSearch.Result(); result != nil {
		mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta")
		return result, nil
	}
	selected := map[id.Zid]*meta.Meta{}
	for _, term := range compSearch.Terms {
		rejected := id.NewSet()
		handleMeta := func(m *meta.Meta) {
			zid := m.Zid
			if rejected.Contains(zid) {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected")
				return
			}
			if _, ok := selected[zid]; ok {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected")
				return
			}
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
	result = compSearch.AfterSearch(result)
	mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta")
	return result, nil
}

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

}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}



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

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

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

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

		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

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







|














|
|

>
>
>











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


<
>















|
|







|







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
	result = compSearch.AfterSearch(result)
	mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta")
	return result, nil
}

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

}

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






































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

	if err := mgr.checkContinue(ctx); err != nil {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

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

Changes to box/manager/collect.go.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

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

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







|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

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

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

Changes to box/manager/enrich.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

package manager

import (
	"context"
	"strconv"

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

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

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

		m.Set(api.KeyCreated, computeCreated(m.Zid))
	}

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





}

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







|







<

|
>



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




|







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

28
29
30
31
32
33
34




35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

package manager

import (
	"context"
	"strconv"

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

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

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

	if box.DoEnrich(ctx) {




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

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

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

Changes to box/manager/indexer.go.

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

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

// idxIndexer runs in the background and updates the index data structures.







|

|








|

|








|

|








|

|







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

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

// idxIndexer runs in the background and updates the index data structures.
139
140
141
142
143
144
145

146
147
148
149
150
151
152
153
154
155
156
157
158

159

160
161
162
163
164
165
166
167




168
169
170
171
172
173
174
		if !ok {
			return false
		}
	case _, ok := <-timer.C:
		if !ok {
			return false
		}

		timer.Reset(timerDuration)
	case <-mgr.done:
		if !timer.Stop() {
			<-timer.C
		}
		return false
	}
	return true
}

func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) {
	var cData collectData
	cData.initialize()

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


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





func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
	for _, pair := range m.ComputedPairs() {
		descr := meta.GetDescription(pair.Key)
		if descr.IsProperty() {
			continue
		}







>













>
|
>








>
>
>
>







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
		if !ok {
			return false
		}
	case _, ok := <-timer.C:
		if !ok {
			return false
		}
		// mgr.idxStore.Optimize() // TODO: make it less often, for example once per 10 minutes
		timer.Reset(timerDuration)
	case <-mgr.done:
		if !timer.Stop() {
			<-timer.C
		}
		return false
	}
	return true
}

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

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

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

func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
	for _, pair := range m.ComputedPairs() {
		descr := meta.GetDescription(pair.Key)
		if descr.IsProperty() {
			continue
		}
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
		}
	} else {
		stWords.Add(value)
	}
}

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

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

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

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

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







|
|




|









|










<
<
<
<
<





|
|

|

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
		}
	} else {
		stWords.Add(value)
	}
}

func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
	cData.refs.ForEach(func(ref id.Zid) {
		if mgr.hasZettel(ctx, ref) {
			zi.AddBackRef(ref)
		} else {
			zi.AddDeadRef(ref)
		}
	})
	zi.SetWords(cData.words)
	zi.SetUrls(cData.urls)
}

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






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

func (mgr *Manager) idxCheckZettel(s *id.Set) {
	s.ForEach(func(zid id.Zid) {
		mgr.idxAr.EnqueueZettel(zid)
	})
}

Changes to box/manager/manager.go.

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

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

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







|







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

// ConnectData contains all administration related values.
type ConnectData struct {
	Number   int // number of the box, starting with 1.
	Config   config.Config
	Enricher box.Enricher
	Notify   box.UpdateNotifier
}

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

117
118
119
120
121
122
123

func (mgr *Manager) setState(newState box.StartState) {
	mgr.stateMx.Lock()
	mgr.state = newState
	mgr.stateMx.Unlock()
}


func (mgr *Manager) State() box.StartState {
	mgr.stateMx.RLock()
	state := mgr.state
	mgr.stateMx.RUnlock()
	return state
}








>







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

func (mgr *Manager) setState(newState box.StartState) {
	mgr.stateMx.Lock()
	mgr.state = newState
	mgr.stateMx.Unlock()
}

// State returns the box.StartState of the manager.
func (mgr *Manager) State() box.StartState {
	mgr.stateMx.RLock()
	state := mgr.state
	mgr.stateMx.RUnlock()
	return state
}

138
139
140
141
142
143
144

145
146
147
148
149
150
151
152
		propertyKeys: propertyKeys,

		idxLog:   boxLog.Clone().Str("box", "index").Child(),
		idxStore: createIdxStore(rtConfig),
		idxAr:    newAnteroomQueue(1000),
		idxReady: make(chan struct{}, 1),
	}

	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
			return nil, err
		}
		if p != nil {







>
|







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
		propertyKeys: propertyKeys,

		idxLog:   boxLog.Clone().Str("box", "index").Child(),
		idxStore: createIdxStore(rtConfig),
		idxAr:    newAnteroomQueue(1000),
		idxReady: make(chan struct{}, 1),
	}

	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
			return nil, err
		}
		if p != nil {
209
210
211
212
213
214
215

216
217
218
219
220
221
222
223
224
225
226
227
				reason, zid := ci.Reason, ci.Zid
				mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier")
				if ignoreUpdate(cache, now, reason, zid) {
					mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored")
					continue
				}


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







>




|







211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
				reason, zid := ci.Reason, ci.Zid
				mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier")
				if ignoreUpdate(cache, now, reason, zid) {
					mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored")
					continue
				}

				isStarted := mgr.State() == box.StartStateStarted
				mgr.idxEnqueue(reason, zid)
				if ci.Box == nil {
					ci.Box = mgr
				}
				if isStarted {
					mgr.notifyObserver(&ci)
				}
			}
		case <-mgr.done:
			return
		}
	}
249
250
251
252
253
254
255


256
257
258
259
260
261
262
func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
	switch reason {
	case box.OnReady:
		return
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:


		mgr.idxAr.EnqueueZettel(zid)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:







>
>







252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
	switch reason {
	case box.OnReady:
		return
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
	case box.OnDelete:
		mgr.idxAr.EnqueueZettel(zid)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
302
303
304
305
306
307
308

309
310
311
312
313
314
315
	}
	mgr.idxAr.Reset() // Ensure an initial index run
	mgr.done = make(chan struct{})
	go mgr.notifier()

	mgr.waitBoxesAreStarted()
	mgr.setState(box.StartStateStarted)

	mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady})

	go mgr.idxIndexer()
	return nil
}

func (mgr *Manager) waitBoxesAreStarted() {







>







307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
	}
	mgr.idxAr.Reset() // Ensure an initial index run
	mgr.done = make(chan struct{})
	go mgr.notifier()

	mgr.waitBoxesAreStarted()
	mgr.setState(box.StartStateStarted)

	mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady})

	go mgr.idxIndexer()
	return nil
}

func (mgr *Manager) waitBoxesAreStarted() {
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
	return true
}

// Stop the started box. Now only the Start() function is allowed.
func (mgr *Manager) Stop(ctx context.Context) {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if mgr.State() != box.StartStateStarted {
		return
	}
	mgr.setState(box.StartStateStopping)
	close(mgr.done)
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			ss.Stop(ctx)
		}
	}
	mgr.setState(box.StartStateStopped)
}

// Refresh internal box data.
func (mgr *Manager) Refresh(ctx context.Context) error {
	mgr.mgrLog.Debug().Msg("Refresh")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil
}

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

// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	mgr.mgrLog.Debug().Msg("ReadStats")
	mgr.mgrMx.RLock()







|















|
|













|

|
|

|







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

// Stop the started box. Now only the Start() function is allowed.
func (mgr *Manager) Stop(ctx context.Context) {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if err := mgr.checkContinue(ctx); err != nil {
		return
	}
	mgr.setState(box.StartStateStopping)
	close(mgr.done)
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			ss.Stop(ctx)
		}
	}
	mgr.setState(box.StartStateStopped)
}

// Refresh internal box data.
func (mgr *Manager) Refresh(ctx context.Context) error {
	mgr.mgrLog.Debug().Msg("Refresh")
	if err := mgr.checkContinue(ctx); err != nil {
		return err
	}
	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil
}

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

// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	mgr.mgrLog.Debug().Msg("ReadStats")
	mgr.mgrMx.RLock()
415
416
417
418
419
420
421













	st.IndexedUrls = storeSt.Urls
}

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




















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

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

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

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

Changes to box/manager/mapstore/mapstore.go.

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




48


49


50
51
52
53
54
55
56
57
58
59
60
61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// Package mapstore stored the index in main memory via a Go map.
package mapstore

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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





type stringRefs map[string]id.Slice





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

	// Stats
	mxStats sync.Mutex
	updates uint64
}


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

func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	if zi, found := ms.idx[zid]; found && zi.meta != nil {
		// zi.meta is nil, if zettel was referenced, but is not indexed yet.
		return zi.meta.Clone(), nil
	}
	return nil, box.ErrZettelNotFound{Zid: zid}
}

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

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

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

	addBackwardZids(result, zid, zi)
	return result
}

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







|



|
|








|
|
|






|
|


>
>
>
>
|
>
>
|
>
>
|



|







>



|


|





|









|







|







|
|



|
|


|
|
|



|
|
|



|
|







|




|


|










|
<




|







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

159
160
161
162
163
164
165
166
167
168
169
170
// Package mapstore stored the index in main memory via a Go map.
package mapstore

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

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

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

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

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

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

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

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

func (ms *mapStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	if zi, found := ms.idx[zid]; found && zi.meta != nil {
		// zi.meta is nil, if zettel was referenced, but is not indexed yet.
		return zi.meta.Clone(), nil
	}
	return nil, box.ErrZettelNotFound{Zid: zid}
}

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

func (ms *mapStore) doEnrich(m *meta.Meta) bool {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	zi, ok := ms.idx[m.Zid]
	if !ok {
		return false
	}
	var updated bool
	if !zi.dead.IsEmpty() {
		m.Set(api.KeyDead, zi.dead.MetaString())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Clone())
	if !zi.backward.IsEmpty() {
		m.Set(api.KeyBackward, zi.backward.MetaString())
		updated = true
	}
	if !zi.forward.IsEmpty() {
		m.Set(api.KeyForward, zi.forward.MetaString())
		back.ISubstract(zi.forward)
		updated = true
	}
	for k, refs := range zi.otherRefs {
		if !refs.backward.IsEmpty() {
			m.Set(k, refs.backward.MetaString())
			back.ISubstract(refs.backward)
			updated = true
		}
	}
	if !back.IsEmpty() {
		m.Set(api.KeyBack, back.MetaString())
		updated = true
	}
	return updated
}

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

	return addBackwardZids(result, zid, zi)

}

// SearchPrefix returns all zettel that have a word with the given prefix.
// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *mapStore) SearchPrefix(prefix string) *id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(prefix, strings.HasPrefix)
	l := len(prefix)
	if l > 14 {
		return result
	}
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243










244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413

414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567

568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
















615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657


658




659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
		minZid, err = id.Parse(prefix + "00000000000000"[:14-l])
		if err != nil {
			return result
		}
	}
	for zid, zi := range ms.idx {
		if minZid <= zid && zid <= maxZid {
			addBackwardZids(result, zid, zi)
		}
	}
	return result
}

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

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

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










	}
	return result
}

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

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

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

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

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

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

	return toCheck
}

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

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

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

func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta {
	origM := zidx.GetMeta()
	copyM := meta.New(origM.Zid)
	for _, p := range origM.Pairs() {
		key := ms.internString(p.Key)
		if isInternableValue(key) {
			copyM.Set(key, ms.internString(p.Value))
		} else if key == api.KeyBoxNumber || !meta.IsComputed(key) {
			copyM.Set(key, p.Value)
		}
	}
	return copyM
}

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

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

	var toCheck id.Set
	for _, ref := range remRefs {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	}
	for _, ref := range newRefs {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	}
	return toCheck
}

func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set {
	// Must only be called if ms.mx is write-locked!
	inverseRefs := zidx.GetInverseRefs()
	for key, mr := range zi.otherRefs {
		if _, ok := inverseRefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}
	if zi.otherRefs == nil {
		zi.otherRefs = make(map[string]bidiRefs)
	}
	var toCheck id.Set
	for key, mrefs := range inverseRefs {
		mr := zi.otherRefs[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.otherRefs[key] = mr

		for _, ref := range newRefs {
			bzi := ms.getOrCreateEntry(ref)
			if bzi.otherRefs == nil {
				bzi.otherRefs = make(map[string]bidiRefs)
			}
			bmr := bzi.otherRefs[key]
			bmr.backward = addRef(bmr.backward, zidx.Zid)
			bzi.otherRefs[key] = bmr
			if bzi.meta == nil {
				toCheck = toCheck.Add(ref)
			}

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

func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {
		if refs, ok := srefs[word]; ok {
			srefs[word] = addRef(refs, zid)
			continue
		}
		srefs[word] = id.Slice{zid}
	}
	for _, word := range removeWords {
		refs, ok := srefs[word]
		if !ok {
			continue
		}
		refs2 := remRef(refs, zid)
		if len(refs2) == 0 {
			delete(srefs, word)
			continue
		}
		srefs[word] = refs2
	}
	return next.Words()
}

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

func (ms *memStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()

	curZi, curFound := ms.idx[curZid]
	_, newFound := ms.idx[newZid]
	if !curFound || newFound {
		return nil
	}
	newZi := &zettelData{
		meta:      copyMeta(curZi.meta, newZid),
		dead:      ms.copyDeadReferences(curZi.dead),
		forward:   ms.copyForward(curZi.forward, newZid),
		backward:  nil, // will be done through tocheck
		otherRefs: nil, // TODO: check if this will be done through toCheck
		words:     copyStrings(ms.words, curZi.words, newZid),
		urls:      copyStrings(ms.urls, curZi.urls, newZid),
	}

	ms.idx[newZid] = newZi
	toCheck := ms.doDeleteZettel(curZid)
	toCheck = toCheck.CopySlice(ms.dead[newZid])
	delete(ms.dead, newZid)
	toCheck = toCheck.Add(newZid) // should update otherRefs
	return toCheck
}
func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta {
	result := m.Clone()
	result.Zid = newZid
	return result
}
func (ms *memStore) copyDeadReferences(curDead id.Slice) id.Slice {
	// Must only be called if ms.mx is write-locked!
	if l := len(curDead); l > 0 {
		result := make(id.Slice, l)
		for i, ref := range curDead {
			result[i] = ref
			ms.dead[ref] = addRef(ms.dead[ref], ref)
		}
		return result
	}
	return nil
}
func (ms *memStore) copyForward(curForward id.Slice, newZid id.Zid) id.Slice {
	// Must only be called if ms.mx is write-locked!
	if l := len(curForward); l > 0 {
		result := make(id.Slice, l)
		for i, ref := range curForward {
			result[i] = ref
			if fzi, found := ms.idx[ref]; found {
				fzi.backward = addRef(fzi.backward, newZid)
			}
		}
		return result
	}
	return nil
}
func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string {
	// Must only be called if ms.mx is write-locked!
	if l := len(curStrings); l > 0 {
		result := make([]string, l)
		for i, s := range curStrings {
			result[i] = s
			msStringMap[s] = addRef(msStringMap[s], newZid)
		}
		return result
	}
	return nil
}

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

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

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

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

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

	}
	var toCheck id.Set
	for _, ref := range zi.backward {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = remRef(bzi.forward, zid)
			toCheck = toCheck.Add(ref)
		}
	}
	return toCheck
}

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

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
















	}
}

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

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

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

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


		for k, fb := range zi.otherRefs {




			fmt.Fprintln(w, "* Meta", k)
			dumpZids(w, "** Forward:", fb.forward)
			dumpZids(w, "** Backward:", fb.backward)
		}
		dumpStrings(w, "* Words", "", "", zi.words)
		dumpStrings(w, "* URLs", "[[", "]]", zi.urls)
	}
}

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

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

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

}

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







|







|

















|







|











|





|






|





|
>
>
>
>
>
>
>
>
>
>




<
<
<
<
<
<
<
<
<
|




|




|







|










|


|






|

|







|


















|







|













|


|

|
|
|
|
|
|


|


|


|
|

|



|
|

|



|



|











|


|



|





|




>
|








<
<
<
<
|






|
|



|




|









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





|

















|

|

|
<
|

|


|


|

|

|

>
|
|
|

|


|



|

|


|



|

|
|







|









|
|



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



|










|










|












|


|
|
>
>
|
>
>
>
>

|
|






|















|
|

|


|



<




|





<












179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265









266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432




433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459






































































460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488

489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651

652
653
654
655
656
657
658
659
660
661

662
663
664
665
666
667
668
669
670
671
672
673
		minZid, err = id.Parse(prefix + "00000000000000"[:14-l])
		if err != nil {
			return result
		}
	}
	for zid, zi := range ms.idx {
		if minZid <= zid && zid <= maxZid {
			result = addBackwardZids(result, zid, zi)
		}
	}
	return result
}

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

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

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

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










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

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

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

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

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

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

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

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

func (ms *mapStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta {
	origM := zidx.GetMeta()
	copyM := meta.New(origM.Zid)
	for _, p := range origM.Pairs() {
		key := ms.internString(p.Key)
		if isInternableValue(key) {
			copyM.Set(key, ms.internString(p.Value))
		} else if key == api.KeyBoxNumber || !meta.IsComputed(key) {
			copyM.Set(key, p.Value)
		}
	}
	return copyM
}

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

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

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

func (ms *mapStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) *id.Set {
	// Must only be called if ms.mx is write-locked!
	inverseRefs := zidx.GetInverseRefs()
	for key, mr := range zi.otherRefs {
		if _, ok := inverseRefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}
	if zi.otherRefs == nil {
		zi.otherRefs = make(map[string]bidiRefs)
	}
	var toCheck *id.Set
	for key, mrefs := range inverseRefs {
		mr := zi.otherRefs[key]
		newRefs, remRefs := mr.forward.Diff(mrefs)
		mr.forward = mrefs
		zi.otherRefs[key] = mr

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

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

func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {




		srefs[word] = srefs[word].Add(zid)
	}
	for _, word := range removeWords {
		refs, ok := srefs[word]
		if !ok {
			continue
		}
		refs = refs.Remove(zid)
		if refs.IsEmpty() {
			delete(srefs, word)
			continue
		}
		srefs[word] = refs
	}
	return next.Words()
}

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







































































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

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

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

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

				delete(ms.dead, ref)
			} else {
				ms.dead[ref] = drefs
			}
		}
	})
}

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

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

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

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

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

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

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

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

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

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

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

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

func dumpSet(w io.Writer, prefix string, s *id.Set) {
	if !s.IsEmpty() {
		io.WriteString(w, prefix)
		s.ForEach(func(zid id.Zid) {
			io.WriteString(w, " ")
			w.Write(zid.Bytes())
		})
		fmt.Fprintln(w)
	}
}

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

}

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

Deleted box/manager/mapstore/refs.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package mapstore

import (
	"slices"

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

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

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

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

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


















































































































































































































Deleted box/manager/mapstore/refs_test.go.

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

package mapstore

import (
	"testing"

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

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

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

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

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

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
























































































































































































































































































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

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



63
64
65
66
67
68
69
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)

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

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

	// RenameZettel changes all references of current zettel identifier to new
	// zettel identifier.
	RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set

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




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

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







|
<
<
<
<



|
>
>
>







47
48
49
50
51
52
53
54




55
56
57
58
59
60
61
62
63
64
65
66
67
68
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)

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

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





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

	// Optimize removes unneeded space.
	Optimize()

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

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

Changes to box/manager/store/wordset_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
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package store_test

import (
	"sort"
	"testing"

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

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







|












|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package store_test

import (
	"slices"
	"testing"

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

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

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

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import (
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

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







|
|
|
|
|










|






|
<
<







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


46
47
48
49
50
51
52
import (
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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



// AddInverseRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) {
	if zids, ok := zi.inverseRefs[key]; ok {
		zids.Add(zid)
		return
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// SetWords sets the words to the given value.
func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words }

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

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

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

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

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

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

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







|





|


|



|

|









62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// SetWords sets the words to the given value.
func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words }

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

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

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

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

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

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

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

Changes to box/membox/membox.go.

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
	maxZettel int
	maxBytes  int
	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]zettel.Zettel
	curBytes  int
}

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

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








|
|
|







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
	maxZettel int
	maxBytes  int
	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]zettel.Zettel
	curBytes  int
}

func (mb *memBox) notifyChanged(zid id.Zid, reason box.UpdateReason) {
	if notify := mb.cdata.Notify; notify != nil {
		notify(mb, zid, reason)
	}
}

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

111
112
113
114
115
116
117

118
119
120
121
122
123
124
125
	}
	meta := zettel.Meta.Clone()
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()

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

func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	mb.mx.RLock()
	z, ok := mb.zettel[zid]







>
|







111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
	}
	meta := zettel.Meta.Clone()
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()

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

func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	mb.mx.RLock()
	z, ok := mb.zettel[zid]
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
		return box.ErrCapacity
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid)
	mb.log.Trace().Msg("UpdateZettel")
	return nil
}

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

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

	// Check that there is no zettel with newZid
	if _, ok = mb.zettel[newZid]; ok {
		mb.mx.Unlock()
		return box.ErrInvalidZid{Zid: newZid.String()}
	}

	meta := zettel.Meta.Clone()
	meta.Zid = newZid
	zettel.Meta = meta
	mb.zettel[newZid] = zettel
	delete(mb.zettel, curZid)
	mb.mx.Unlock()
	mb.notifyChanged(curZid)
	mb.notifyChanged(newZid)
	mb.log.Trace().Msg("RenameZettel")
	return nil
}

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

func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	mb.mx.Lock()
	oldZettel, found := mb.zettel[zid]
	if !found {
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	mb.log.Trace().Msg("DeleteZettel")
	return nil
}

func (mb *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mb.mx.RLock()
	st.Zettel = len(mb.zettel)
	mb.mx.RUnlock()
	mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}







|




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

















|











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
		return box.ErrCapacity
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid, box.OnZettel)
	mb.log.Trace().Msg("UpdateZettel")
	return nil
}





























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

func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	mb.mx.Lock()
	oldZettel, found := mb.zettel[zid]
	if !found {
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(zid, box.OnDelete)
	mb.log.Trace().Msg("DeleteZettel")
	return nil
}

func (mb *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mb.mx.RLock()
	st.Zettel = len(mb.zettel)
	mb.mx.RUnlock()
	mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

Changes to box/notify/directory.go.

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

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

	"zettelstore.de/z/box"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"







<







14
15
16
17
18
19
20

21
22
23
24
25
26
27
package notify

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

	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// dsCreated --Start--> dsStarting
// dsStarting --last list notification--> dsWorking
// dsWorking --directory missing--> dsMissing
// dsMissing --last list notification--> dsWorking
// --Stop--> dsStopping
type DirServiceState uint8


const (
	DsCreated  DirServiceState = iota
	DsStarting                 // Reading inital scan
	DsWorking                  // Initial scan complete, fully operational
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    chan<- box.UpdateInfo
	mx       sync.RWMutex // protects status, entries
	state    DirServiceState
	entries  entrySet
}

// ErrNoDirectory signals missing directory data.
var ErrNoDirectory = errors.New("unable to retrieve zettel directory information")

// NewDirService creates a new directory service.
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    chci,
		state:    DsCreated,
	}
}

// State the current service state.
func (ds *DirService) State() DirServiceState {
	ds.mx.RLock()







>














|









|




|







38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// dsCreated --Start--> dsStarting
// dsStarting --last list notification--> dsWorking
// dsWorking --directory missing--> dsMissing
// dsMissing --last list notification--> dsWorking
// --Stop--> dsStopping
type DirServiceState uint8

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

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    box.UpdateNotifier
	mx       sync.RWMutex // protects status, entries
	state    DirServiceState
	entries  entrySet
}

// ErrNoDirectory signals missing directory data.
var ErrNoDirectory = errors.New("unable to retrieve zettel directory information")

// NewDirService creates a new directory service.
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    notify,
		state:    DsCreated,
	}
}

// State the current service state.
func (ds *DirService) State() DirServiceState {
	ds.mx.RLock()
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
	if ds.entries == nil {
		return ds.logMissingEntry("update")
	}
	ds.entries[entry.Zid] = &entry
	return nil
}

// RenameDirEntry replaces an existing directory entry with a new one.
func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return DirEntry{}, ds.logMissingEntry("rename")
	}
	if _, found := ds.entries[newZid]; found {
		return DirEntry{}, box.ErrInvalidZid{Zid: newZid.String()}
	}
	oldZid := oldEntry.Zid
	newEntry := DirEntry{
		Zid:         newZid,
		MetaName:    renameFilename(oldEntry.MetaName, oldZid, newZid),
		ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid),
		ContentExt:  oldEntry.ContentExt,
		// Duplicates must not be set, because duplicates will be deleted
	}
	delete(ds.entries, oldZid)
	ds.entries[newZid] = &newEntry
	return newEntry, nil
}

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

// DeleteDirEntry removes a entry from the directory.
func (ds *DirService) DeleteDirEntry(zid id.Zid) error {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return ds.logMissingEntry("delete")
	}







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







182
183
184
185
186
187
188






























189
190
191
192
193
194
195
	if ds.entries == nil {
		return ds.logMissingEntry("update")
	}
	ds.entries[entry.Zid] = &entry
	return nil
}































// DeleteDirEntry removes a entry from the directory.
func (ds *DirService) DeleteDirEntry(zid id.Zid) error {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return ds.logMissingEntry("delete")
	}
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
		ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing")
		return nil, true
	case Update:
		ds.mx.Lock()
		zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid)
		}
	case Delete:
		ds.mx.Lock()
		zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid)
		}
	default:
		ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
	}
	return newEntries, true
}

func getNewZids(entries entrySet) id.Slice {
	zids := make(id.Slice, 0, len(entries))
	for zid := range entries {
		zids = append(zids, zid)
	}
	return zids
}

func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) {
	for _, zid := range zids {
		ds.notifyChange(zid)
		delete(prevEntries, zid)
	}

	// These were previously stored, by are not found now.
	// Notify system that these were deleted, e.g. for updating the index.
	for zid := range prevEntries {
		ds.notifyChange(zid)
	}
}

func (ds *DirService) onDestroyDirectory() {
	ds.mx.Lock()
	entries := ds.entries
	ds.entries = nil
	ds.state = DsMissing
	ds.mx.Unlock()
	for zid := range entries {
		ds.notifyChange(zid)
	}
}

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

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)







|






|

















|






|










|







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
		ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing")
		return nil, true
	case Update:
		ds.mx.Lock()
		zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnZettel)
		}
	case Delete:
		ds.mx.Lock()
		zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnDelete)
		}
	default:
		ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
	}
	return newEntries, true
}

func getNewZids(entries entrySet) id.Slice {
	zids := make(id.Slice, 0, len(entries))
	for zid := range entries {
		zids = append(zids, zid)
	}
	return zids
}

func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) {
	for _, zid := range zids {
		ds.notifyChange(zid, box.OnZettel)
		delete(prevEntries, zid)
	}

	// These were previously stored, by are not found now.
	// Notify system that these were deleted, e.g. for updating the index.
	for zid := range prevEntries {
		ds.notifyChange(zid, box.OnDelete)
	}
}

func (ds *DirService) onDestroyDirectory() {
	ds.mx.Lock()
	entries := ds.entries
	ds.entries = nil
	ds.state = DsMissing
	ds.mx.Unlock()
	for zid := range entries {
		ds.notifyChange(zid, box.OnDelete)
	}
}

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

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
601
602
603
604
605
606
607
608
609
610
611
612
613
	newLen := len(newExt)
	if oldLen != newLen {
		return newLen < oldLen
	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(zid id.Zid) {
	if chci := ds.infos; chci != nil {
		ds.log.Trace().Zid(zid).Msg("notifyChange")
		chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid}
	}
}







|
|
|
|


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

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

Changes to box/notify/entry.go.

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

package notify

import (
	"path/filepath"

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

const (







|







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

package notify

import (
	"path/filepath"

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

const (

Changes to cmd/cmd_file.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import (
	"context"
	"flag"
	"fmt"
	"io"
	"os"

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








|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import (
	"context"
	"flag"
	"fmt"
	"io"
	"os"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

Changes to cmd/cmd_password.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import (
	"flag"
	"fmt"
	"os"

	"golang.org/x/term"

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

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

func cmdPassword(fs *flag.FlagSet) (int, error) {







|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import (
	"flag"
	"fmt"
	"os"

	"golang.org/x/term"

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

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

func cmdPassword(fs *flag.FlagSet) (int, error) {

Changes to cmd/cmd_run.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
106
107
	ucQuery.SetEvaluate(&ucEvaluate)
	ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery)
	ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery)
	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager)
	ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager)
	ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager)
	ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	a := api.New(
		webLog.Clone().Str("adapter", "api").Child(),
		webSrv, authManager, authManager, rtConfig, authPolicy)
	wui := webui.New(
		webLog.Clone().Str("adapter", "wui").Child(),
		webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate)

	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
	if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" {
		const assetPrefix = "/assets/"
		webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir))))
		webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir))
	}

	// Web user interface
	if !authManager.IsReadonly() {
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename))
		webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax))
		webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel))
		webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete))







<




















<
<







71
72
73
74
75
76
77

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


98
99
100
101
102
103
104
	ucQuery.SetEvaluate(&ucEvaluate)
	ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery)
	ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery)
	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager)

	ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager)
	ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	a := api.New(
		webLog.Clone().Str("adapter", "api").Child(),
		webSrv, authManager, authManager, rtConfig, authPolicy)
	wui := webui.New(
		webLog.Clone().Str("adapter", "wui").Child(),
		webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate)

	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
	if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" {
		const assetPrefix = "/assets/"
		webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir))))
		webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir))
	}

	// Web user interface
	if !authManager.IsReadonly() {


		webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax))
		webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel))
		webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete))
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename))
	}

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

type getUserImpl struct{}

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







<










120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135
136
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))

	}

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

type getUserImpl struct{}

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

Changes to cmd/command.go.

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

package cmd

import (
	"flag"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/logger"
)

// Command stores information about commands / sub-commands.
type Command struct {
	Name       string              // command name as it appears on the command line
	Func       CommandFunc         // function that executes a command







|







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

package cmd

import (
	"flag"

	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/logger"
)

// Command stores information about commands / sub-commands.
type Command struct {
	Name       string              // command name as it appears on the command line
	Func       CommandFunc         // function that executes a command

Changes to cmd/main.go.

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/impl"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/compbox"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"







|
|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/impl"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/compbox"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
158
159
160
161
162
163
164

165
166
167
168
169
170
171
172
173
174
175

176
177
178
179
180
181
182
	}
}

const (
	keyAdminPort         = "admin-port"
	keyAssetDir          = "asset-dir"
	keyBaseURL           = "base-url"

	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyInsecureHTML      = "insecure-html"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyReadOnly          = "read-only-mode"

	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) bool {







>









<

>







158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

175
176
177
178
179
180
181
182
183
	}
}

const (
	keyAdminPort         = "admin-port"
	keyAssetDir          = "asset-dir"
	keyBaseURL           = "base-url"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyInsecureHTML      = "insecure-html"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"

	keyReadOnly          = "read-only-mode"
	keyRuntimeProfiling  = "runtime-profiling"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) bool {
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
		val, found := cfg.Get(key)
		if !found {
			break
		}
		err = setConfigValue(err, kernel.BoxService, key, val)
	}


	err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))


	err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	if val, found := cfg.Get(keyBaseURL); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val)
	}
	if val, found := cfg.Get(keyURLPrefix); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val)
	}
	err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))

	if val, found := cfg.Get(keyAssetDir); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val)
	}
	return err == nil
}

func setConfigValue(err error, subsys kernel.Service, key string, val any) error {







>
|

>
|















>







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
		val, found := cfg.Get(key)
		if !found {
			break
		}
		err = setConfigValue(err, kernel.BoxService, key, val)
	}

	err = setConfigValue(
		err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))

	err = setConfigValue(
		err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	if val, found := cfg.Get(keyBaseURL); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val)
	}
	if val, found := cfg.Get(keyURLPrefix); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val)
	}
	err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))
	err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling))
	if val, found := cfg.Get(keyAssetDir); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val)
	}
	return err == nil
}

func setConfigValue(err error, subsys kernel.Service, key string, val any) error {

Changes to cmd/zettelstore/main.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
import (
	"os"

	"zettelstore.de/z/cmd"
)

// Version variable. Will be filled by build process.
var version string = ""

func main() {
	exitCode := cmd.Main("Zettelstore", version)
	os.Exit(exitCode)
}







|





17
18
19
20
21
22
23
24
25
26
27
28
29
import (
	"os"

	"zettelstore.de/z/cmd"
)

// Version variable. Will be filled by build process.
var version string

func main() {
	exitCode := cmd.Main("Zettelstore", version)
	os.Exit(exitCode)
}

Changes to collect/order.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
//-----------------------------------------------------------------------------

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

// Order of internal reference within the given zettel.
func Order(zn *ast.ZettelNode) (result []*ast.Reference) {
	for _, bn := range zn.Ast {
		ln, ok := bn.(*ast.NestedListNode)
		if !ok {
			continue
		}
		switch ln.Kind {
		case ast.NestedListOrdered, ast.NestedListUnordered:
			for _, is := range ln.Items {
				if ref := firstItemZettelReference(is); ref != nil {
					result = append(result, ref)
				}
			}
		}
	}
	return result
}

func firstItemZettelReference(is ast.ItemSlice) *ast.Reference {
	for _, in := range is {
		if pn, ok := in.(*ast.ParaNode); ok {
			if ref := firstInlineZettelReference(pn.Inlines); ref != nil {
				return ref
			}
		}
	}
	return nil
}

func firstInlineZettelReference(is ast.InlineSlice) (result *ast.Reference) {
	for _, inl := range is {
		switch in := inl.(type) {
		case *ast.LinkNode:
			if ref := in.Ref; ref.IsZettel() {
				return ref
			}
			result = firstInlineZettelReference(in.Inlines)
		case *ast.EmbedRefNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.EmbedBLOBNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:
			result = firstInlineZettelReference(in.Inlines)
		default:
			continue
		}
		if result != nil {
			return result
		}
	}







|
|








|
|







|


|
|






|



<
|
<
<

|

|

|




|







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

53


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

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

// Order of internal links within the given zettel.
func Order(zn *ast.ZettelNode) (result []*ast.LinkNode) {
	for _, bn := range zn.Ast {
		ln, ok := bn.(*ast.NestedListNode)
		if !ok {
			continue
		}
		switch ln.Kind {
		case ast.NestedListOrdered, ast.NestedListUnordered:
			for _, is := range ln.Items {
				if ln := firstItemZettelLink(is); ln != nil {
					result = append(result, ln)
				}
			}
		}
	}
	return result
}

func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode {
	for _, in := range is {
		if pn, ok := in.(*ast.ParaNode); ok {
			if ln := firstInlineZettelLink(pn.Inlines); ln != nil {
				return ln
			}
		}
	}
	return nil
}

func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) {
	for _, inl := range is {
		switch in := inl.(type) {
		case *ast.LinkNode:

			return in


		case *ast.EmbedRefNode:
			result = firstInlineZettelLink(in.Inlines)
		case *ast.EmbedBLOBNode:
			result = firstInlineZettelLink(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelLink(in.Inlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:
			result = firstInlineZettelLink(in.Inlines)
		default:
			continue
		}
		if result != nil {
			return result
		}
	}

Changes to config/config.go.

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

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

// Key values that are supported by Config.Get
const (
	KeyFooterZettel         = "footer-zettel"
	KeyHomeZettel           = "home-zettel"
	KeyShowBackLinks        = "show-back-links"
	KeyShowFolgeLinks       = "show-folge-links"
	KeyShowSubordinateLinks = "show-subordinate-links"
	KeyShowSuccessorLinks   = "show-successor-links"
	// api.KeyLang
)

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig








|
|
|
|
|
|







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

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

// Key values that are supported by Config.Get
const (
	KeyFooterZettel       = "footer-zettel"
	KeyHomeZettel         = "home-zettel"
	KeyShowBackLinks      = "show-back-links"
	KeyShowFolgeLinks     = "show-folge-links"
	KeyShowSequelLinks    = "show-sequel-links"
	KeyShowSuccessorLinks = "show-successor-links"
	// api.KeyLang
)

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig

Changes to docs/development/20210916193200.zettel.

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

28
id: 20210916193200
title: Required Software
role: zettel
syntax: zmk
created: 20210916193200
modified: 20231213194509

The following software must be installed:

* A current, supported [[release of Go|https://go.dev/doc/devel/release]],
* [[Fossil|https://fossil-scm.org/]],
* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only).

Make sure that the software is in your path, e.g. via:
```sh
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin
```

The internal build tool need the following software.
It can be installed / updated via the build tool itself: ``go run tools/devtools/devtools.go``.

Otherwise you can install the software by hand:

* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``,
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``,

* [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``,





|













|
|






>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
id: 20210916193200
title: Required Software
role: zettel
syntax: zmk
created: 20210916193200
modified: 20241213124936

The following software must be installed:

* A current, supported [[release of Go|https://go.dev/doc/devel/release]],
* [[Fossil|https://fossil-scm.org/]],
* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only).

Make sure that the software is in your path, e.g. via:
```sh
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin
```

The internal build tool needs the following software tools.
They can be installed / updated via the build tool itself: ``go run tools/devtools/devtools.go``.

Otherwise you can install the software by hand:

* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``,
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``,
* [[revive|https://revive.run]] via ``go install github.com/mgechev/revive@vlatest``,
* [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``,

Changes to docs/development/20210916194900.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
id: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk
created: 20210916194900
modified: 20231213194631

# Sync with the official repository
#* ``fossil sync -u``
# Make sure that there is no workspace defined.
#* ``ls ..`` must not have a file ''go.work'', in no parent folder.
# Make sure that all dependencies are up-to-date.
#* ``cat go.mod``
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``).
# All internal tests must succeed:
#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``).
# The API tests must succeed on every development platform:
#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``).
# Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual:
#* ``go run -race cmd/zettelstore/main.go run -d docs/manual``
#* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt``
#* Check all ""Error: 404 Not Found""
#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''.
#* Try to resolve other error messages and warnings
#* Warnings about empty content can be ignored
# On every development platform, the box with 10.000 zettel must run, with ''-race'' enabled:
#* ``go run -race cmd/zettelstore/main.go run -d DIR``.
# Create a development release:
#* ``go run tools/build.go release`` (alternatively: ``make release``).
# On every platform (esp. macOS), the box with 10.000 zettel must run properly:
#* ``./zettelstore -d DIR``
# Update files in directory ''www''
#* index.wiki
#* download.wiki
#* changes.wiki
#* plan.wiki
# Set file ''VERSION'' to the new release version.
  It _must_ consist of three digits: MAJOR.MINOR.PATCH, even if PATCH is zero
# Disable Fossil autosync mode:
#* ``fossil setting autosync off``
# Commit the new release version:
#* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"``
#* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''.
   Otherwise client will not be able to import ''zettelkasten.de/z''.
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``).
# Create the release:
#* ``go run tools/build/build.go release`` (alternatively: ``make release``).
# Remove previous executables:
#* ``fossil uv remove --glob '*-PREVVERSION*'``
# Add executables for release:
#* ``cd releases``
#* ``fossil uv add *.zip``
#* ``cd ..``
#* Synchronize with main repository:





|

|

|

|


|

|

|




|



|

|


|
|
|
|
|

|





|

|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk
created: 20210916194900
modified: 20241213125640

# Sync with the official repository:
#* ``fossil sync -u``
# Make sure that there is no workspace defined:
#* ``ls ..`` must not have a file ''go.work'', in no parent folder.
# Make sure that all dependencies are up-to-date:
#* ``cat go.mod``
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``)
# All internal tests must succeed:
#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``)
# The API tests must succeed on every development platform:
#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``)
# Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual:
#* ``go run -race cmd/zettelstore/main.go run -d docs/manual``
#* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt``
#* Check all ""Error: 404 Not Found""
#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/z'' for those zettel that are accessible only in ''expert-mode''
#* Try to resolve other error messages and warnings
#* Warnings about empty content can be ignored
# On every development platform, the box with 10.000 zettel must run, with ''-race'' enabled:
#* ``go run -race cmd/zettelstore/main.go run -d DIR``
# Create a development release:
#* ``go run tools/build.go release`` (alternatively: ``make release``)
# On every platform (esp. macOS), the box with 10.000 zettel must run properly:
#* ``./zettelstore -d DIR``
# Update files in directory ''www'':
#* ''index.wiki''
#* ''download.wiki''
#* ''changes.wiki''
#* ''plan.wiki''
# Set file ''VERSION'' to the new release version.
  It **must** consists of three numbers: ''MAJOR.MINOR.PATCH'', even if ''PATCH'' is zero.
# Disable Fossil autosync mode:
#* ``fossil setting autosync off``
# Commit the new release version:
#* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"``
#* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''.
   Otherwise client software will not be able to import ''zettelstore.de/z''.
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``)
# Create the release:
#* ``go run tools/build/build.go release`` (alternatively: ``make release``)
# Remove previous executables:
#* ``fossil uv remove --glob '*-PREVVERSION*'``
# Add executables for release:
#* ``cd releases``
#* ``fossil uv add *.zip``
#* ``cd ..``
#* Synchronize with main repository:

Changes to docs/development/20231218181900.zettel.

67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
This list is used to check the generated HTML code (''ZID'' is the paceholder for the zettel identification):

* Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel''
* Check all zettel web views, via the path ''/h/ZID''
* The info page of all zettel is checked, via path ''/i/ZID''
* A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID''
* 10 random zettel are checked for a valid create form, via ''/c/ZID''
* The zettel rename form will be checked for 100 zettel, via ''/b/ZID''
* A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID''

Depending on the selected Zettelstore, the command might take a long time.

You can shorten the time, if you disable any zettel query in the footer.

=== Build







<







67
68
69
70
71
72
73

74
75
76
77
78
79
80
This list is used to check the generated HTML code (''ZID'' is the paceholder for the zettel identification):

* Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel''
* Check all zettel web views, via the path ''/h/ZID''
* The info page of all zettel is checked, via path ''/i/ZID''
* A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID''
* 10 random zettel are checked for a valid create form, via ''/c/ZID''

* A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID''

Depending on the selected Zettelstore, the command might take a long time.

You can shorten the time, if you disable any zettel query in the footer.

=== Build

Changes to docs/manual/00000000000100.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
created: 00010101000000
default-copyright: (c) 2020-present by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-zettel: 00001000000100
home-zettel: 00001000000000
modified: 20221205173642
site-name: Zettelstore Manual




|







1
2
3
4
5
6
7
8
9
10
11
12
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
created: 20210126175322
default-copyright: (c) 2020-present by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-zettel: 00001000000100
home-zettel: 00001000000000
modified: 20221205173642
site-name: Zettelstore Manual

Changes to docs/manual/00001000000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210301190630
modified: 20231125185455
show-back-links: false

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

Version: {{00001000000001}}.

Licensed under the EUPL-1.2-or-later.





|
|

















|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241128141924
show-back-links: false

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

Version: {{00001000000001}}

Licensed under the EUPL-1.2-or-later.

Added docs/manual/00001000000002.zettel.















>
>
>
>
>
>
>
1
2
3
4
5
6
7
id: 00001000000002
title: manual
role: role
syntax: zmk
created: 20231128184200

Zettel with the role ""manual"" contain the manual of the zettelstore.

Changes to docs/manual/00001001000000.zettel.

1
2
3
4
5


6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001001000000
title: Introduction to the Zettelstore
role: manual
tags: #introduction #manual #zettelstore
syntax: zmk



[[Personal knowledge
management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is
about collecting, classifying, storing, searching, retrieving, assessing,
evaluating, and sharing knowledge as a daily activity. Personal knowledge
management is done by most people, not necessarily as part of their main
business. It is essential for knowledge workers, like students, researchers,
lecturers, software developers, scientists, engineers, architects, to name
a few. Many hobbyists build up a significant amount of knowledge, even if the
do not need to think for a living. Personal knowledge management can be seen as
a prerequisite for many kinds of collaboration.

Zettelstore is a software that collects and relates your notes (""zettel"")
to represent and enhance your knowledge. It helps with many tasks of personal
knowledge management by explicitly supporting the ""[[Zettelkasten
method|https://en.wikipedia.org/wiki/Zettelkasten]]"". The method is based on
creating many individual notes, each with one idea or information, that are
related to each other. Since knowledge is typically build up gradually, one
major focus is a long-term store of these notes, hence the name
""Zettelstore"".





>
>

<
|
<
<
|
<
|
|
<
|

|
<
<
<
|
<
|
<
1
2
3
4
5
6
7
8

9


10

11
12

13
14
15



16

17

id: 00001001000000
title: Introduction to the Zettelstore
role: manual
tags: #introduction #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240710184612


[[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] involves collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity.


It's done by most individuals, not necessarily as part of their main business.

It's essential for knowledge workers, such as students, researchers, lecturers, software developers, scientists, engineers, architects, etc.
Many hobbyists build up a significant amount of knowledge, even if they do not need to think for a living.

Personal knowledge management can be seen as a prerequisite for many kinds of collaboration.

Zettelstore is software that collects and relates your notes (""zettel"") to represent and enhance your knowledge, supporting the ""[[Zettelkasten method|https://en.wikipedia.org/wiki/Zettelkasten]]"".



The method is based on creating many individual notes, each with one idea or piece of information, that is related to each other.

Since knowledge is typically built up gradually, one major focus is a long-term store of these notes, hence the name ""Zettelstore"".

Changes to docs/manual/00001003000000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12

13
14
15
16
17
18
19
id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220119145756

=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter problem, please take a look on the [[Troubleshooting|00001018000000]] page.]

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





>
|





|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241213101917

=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software.
  If you encounter a problem, please take a look on the [[Troubleshooting|00001018000000]] page.]
* A sub-directory ""zettel"" will be created in the directory where you put the executable.
  It will contain your future zettel.
* Open the URI [[http://localhost:23123]] with your web browser.
  It will present you a mostly empty Zettelstore.
  There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information.
* Please read the instructions for the [[web-based user interface|00001014000000]] and learn about the various ways to write zettel.
* If you restart your device, please make sure to start your Zettelstore again.

Changes to docs/manual/00001003300000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003300000
title: Zettelstore installation for the intermediate user
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220114175754

You already tried the Zettelstore software and now you want to use it permanently.
Zettelstore should start automatically when you log into your computer.

* Grab the appropriate executable and copy it into the appropriate directory
* If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]].





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003300000
title: Zettelstore installation for the intermediate user
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20220114175754

You already tried the Zettelstore software and now you want to use it permanently.
Zettelstore should start automatically when you log into your computer.

* Grab the appropriate executable and copy it into the appropriate directory
* If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]].

Changes to docs/manual/00001003305000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001003305000
title: Enable Zettelstore to start automatically on Windows
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220218125541

Windows is a complicated beast. There are several ways to automatically start Zettelstore.

=== Startup folder

One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd]].
Open the folder where you have placed in the Explorer.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001003305000
title: Enable Zettelstore to start automatically on Windows
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20241213103259

Windows is a complicated beast. There are several ways to automatically start Zettelstore.

=== Startup folder

One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd]].
Open the folder where you have placed in the Explorer.
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

=== Task scheduler

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

This is both an advantage and a disadvantage.

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

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








|







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

=== Task scheduler

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

This is both an advantage and a disadvantage.

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

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

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

Create a new action.

{{00001003305112}}

The next steps are the trickiest.

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

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








|







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

Create a new action.

{{00001003305112}}

The next steps are the trickiest.

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

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

Changes to docs/manual/00001003310000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003310000
title: Enable Zettelstore to start automatically on macOS
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220119124635

There are several ways to automatically start Zettelstore.

* [[Login Items|#login-items]]
* [[Launch Agent|#launch-agent]]






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003310000
title: Enable Zettelstore to start automatically on macOS
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20220114181521
modified: 20220119124635

There are several ways to automatically start Zettelstore.

* [[Login Items|#login-items]]
* [[Launch Agent|#launch-agent]]

Changes to docs/manual/00001003315000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003315000
title: Enable Zettelstore to start automatically on Linux
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220307104944

Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore.

* One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]].
** See below for a lighter alternative.
* If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool"").





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003315000
title: Enable Zettelstore to start automatically on Linux
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20220114181521
modified: 20220307104944

Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore.

* One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]].
** See below for a lighter alternative.
* If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool"").

Changes to docs/manual/00001003600000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003600000
title: Installation of Zettelstore on a server
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20211125185833

You want to provide a shared Zettelstore that can be used from your various devices.
Installing Zettelstore as a Linux service is not that hard.

Grab the appropriate executable and copy it into the appropriate directory:
```sh





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003600000
title: Installation of Zettelstore on a server
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20211125185833

You want to provide a shared Zettelstore that can be used from your various devices.
Installing Zettelstore as a Linux service is not that hard.

Grab the appropriate executable and copy it into the appropriate directory:
```sh

Changes to docs/manual/00001004000000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004000000
title: Configuration of Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20210510153233

There are some levels to change the behavior and/or the appearance of Zettelstore.

# The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface).
#* [[Command line parameters|00001004050000]]
# As an intermediate user, you usually want to have more control over how Zettelstore is started.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004000000
title: Configuration of Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210510153233

There are some levels to change the behavior and/or the appearance of Zettelstore.

# The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface).
#* [[Command line parameters|00001004050000]]
# As an intermediate user, you usually want to have more control over how Zettelstore is started.

Changes to docs/manual/00001004010000.zettel.

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

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124





125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240220190138

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

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

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

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

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

  Default: ""0""
; [!asset-dir|''asset-dir'']
: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server.
  The URL prefix for these files is ''/assets/''.
  You can use this if you want to transfer files that are too large for a note to users.
  Examples would be presentation files, PDF files, music files or video files.

  Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].]

  If you specify only the URL prefix, then the contents of the directory are listed to the user.
  To avoid this, create an empty file in the directory named ""index.html"".

  Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid.
; [!base-url|''base-url'']
: Sets the absolute base URL for the service.

  Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start.
  
  Default: ""http://127.0.0.1:23123/"".
; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one
: Specifies a [[box|00001004011200]] where zettel are stored.
  During startup __X__ is counted up, starting with one, until no key is found.
  This allows to configure more than one box.

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


  Do not enable it for a production server.

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

  Default: ""notify""
; [!insecure-cookie|''insecure-cookie'']
: Must be set to [[true|00001006030500]], if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP).
  Otherwise web browser are free to ignore the authentication cookie.

  Default: ""false""
; [!insecure-html|''insecure-html'']
: Allows to use HTML, e.g. within supported markup languages, even if this might introduce security-related problems.
  However, HTML containing the ``<script>`` or the ``<iframe>`` tag is always ignored.
  But due to ""clever"" ways of combining HTML, CSS, JavaScript, there might be some negative security consequences.
  Please be aware of this!

  Allowed values: ""html"" (allow zettel with [[syntax ""html""|00001008000000#html]]), ""markdown"" (""html"", plus allow inline HTML for Markdown markup only), ""zettelmarkup"" (""markdown"", plus allow inline HTML for Zettelmarkup).
  Any other value is interpreted as ""secure"".

  Default: ""secure"".
; [!listen-addr|''listen-addr'']
: Configures the network address, where the Zettelstore service is listening for requests.
  Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port.

  Default value: ""127.0.0.1:23123""
; [!log-level|''log-level'']
: Specify the [[logging level|00001004059700]] for the whole application or for a given (internal) service, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]].
  Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]].

  Several specifications are separated by the semicolon character (""'';''"", U+003B).
  Each specification consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level.

  Default: ""info"".


  Examples: ""error"" will produce just error messages (e.g. no ""info"" messages); ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components.

  When you are familiar to operate the Zettelstore, you might set the level to ""error"" to receive less noisy messages from the Zettelstore.
; [!max-request-size|''max-request-size'']
: Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources.
  The minimum value is 1024.

  Default: 16777216 (16 MiB). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.
  The owner has full authorization for the Zettelstore.
  Only if owner is set to some value, user [[authentication|00001010000000]] is enabled.

  Ensure that key [[''secret''|#secret]] is set to a value of at least 16 bytes.
  Otherwise the Zettelstore will not start for security reasons.
; [!persistent-cookie|''persistent-cookie'']
: A [[boolean value|00001006030500]] to make the access cookie persistent.
  This is helpful if you access the Zettelstore via a mobile device.
  On these devices, the operating system is free to stop the web browser and to remove temporary cookies.
  Therefore, an authenticated user will be logged off.

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

  Default: ""false""
; [!read-only-mode|''read-only-mode'']
: Puts the Zettelstore service into a read-only mode, if set to a [[true value|00001006030500]].
  No changes are possible.

  Default: ""false"".





; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be modified by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  It is only needed to set this value, if [[authentication is enabled|00001010040100]] by setting key [[''owner''|#owner]] to some user identification.
; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]].
  Default: ""10"".

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

  Note: ''url-prefix'' must be the suffix of [[''base-url''|#base-url]], otherwise the web service will not start.

  Default: ""/"".

  This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore.
; [!verbose-mode|''verbose-mode'']
: Be more verbose when logging data, if set to a [[true value|00001006030500]].

  Default: ""false""






|

|
|
|

|








|








|
<

|

|











|
|




|


>





|
<



|
|














|







|



>
|

|

|






|

|
<



|



|



|



>
>
>
>
>

|


|








|



|










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

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240926144803

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

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

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

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

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

  Default: ""0""
; [!asset-dir|''asset-dir'']
: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server.
  The URL prefix for these files is ''/assets/''.
  You can use this if you want to transfer files that are too large for a zettel, such as presentation, PDF, music or video files.


  Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the very special case that the directory is one of the configured [[boxes|#box-uri-x]].]

  If you specify only the URL prefix in your web client, the contents of the directory are listed.
  To avoid this, create an empty file in the directory named ""index.html"".

  Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid.
; [!base-url|''base-url'']
: Sets the absolute base URL for the service.

  Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start.
  
  Default: ""http://127.0.0.1:23123/"".
; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one
: Specifies a [[box|00001004011200]] where zettel are stored.
  During startup, __X__ is incremented, starting with one, until no key is found.
  This allows to configuring than one box.

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

  Do not enable it for a production server.

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


  Default: ""notify""
; [!insecure-cookie|''insecure-cookie'']
: Must be set to [[true|00001006030500]] if authentication is enabled and Zettelstore is not accessible via HTTPS (but via HTTP).
  Otherwise web browsers are free to ignore the authentication cookie.

  Default: ""false""
; [!insecure-html|''insecure-html'']
: Allows to use HTML, e.g. within supported markup languages, even if this might introduce security-related problems.
  However, HTML containing the ``<script>`` or the ``<iframe>`` tag is always ignored.
  But due to ""clever"" ways of combining HTML, CSS, JavaScript, there might be some negative security consequences.
  Please be aware of this!

  Allowed values: ""html"" (allow zettel with [[syntax ""html""|00001008000000#html]]), ""markdown"" (""html"", plus allow inline HTML for Markdown markup only), ""zettelmarkup"" (""markdown"", plus allow inline HTML for Zettelmarkup).
  Any other value is interpreted as ""secure"".

  Default: ""secure"".
; [!listen-addr|''listen-addr'']
: Configures the network address, where the Zettelstore service is listening for requests.
  The syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces), and ''PORT'' is the TCP port.

  Default value: ""127.0.0.1:23123""
; [!log-level|''log-level'']
: Specify the [[logging level|00001004059700]] for the whole application or for a given (internal) service, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]].
  Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]].

  Several specifications are separated by the semicolon character (""'';''"", U+003B).
  Each consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level.

  Default: ""info"".

  Examples: ""error"" will produce just error messages (e.g. no ""info"" messages).
  ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components.

  When you are familiar with operating the Zettelstore, you might set the level to ""error"" to receive fewer noisy messages from it.
; [!max-request-size|''max-request-size'']
: It limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources.
  The minimum value is 1024.

  Default: 16777216 (16 MiB). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.
  The owner has full authorization for the Zettelstore.
  Only if set to some value, user [[authentication|00001010000000]] is enabled.

  Ensure that the key [[''secret''|#secret]] is set to a value of at least 16 bytes, otherwise the Zettelstore will not start for security reasons.

; [!persistent-cookie|''persistent-cookie'']
: A [[boolean value|00001006030500]] to make the access cookie persistent.
  This is helpful if you access the Zettelstore via a mobile device.
  On these, the operating system is free to stop the web browser and to remove temporary cookies.
  Therefore, an authenticated user will be logged off.

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

  Default: ""false""
; [!read-only-mode|''read-only-mode'']
: If set to a [[true value|00001006030500]] the Zettelstore service puts into a read-only mode.
  No changes are possible.

  Default: ""false"".
; [!runtime-profiling|''runtime-profiling'']
: A boolean value that enables a web interface to obtain [[runtime profiling information|00001004010200]].

  Default: ""false"", but it is set to ""true"" if [[''debug-mode''|#debug-mode]] is enabled.
  In this case, it cannot be disabled.
; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be altered by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  This value is only needed to be set if [[authentication is enabled|00001010040100]] by setting the key [[''owner''|#owner]] to some user identification value.
; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]].
  Default: ""10"".

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

  Note: ''url-prefix'' must be the suffix of [[''base-url''|#base-url]], otherwise the web service will not start.

  Default: ""/"".

  This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore.
; [!verbose-mode|''verbose-mode'']
: Be more verbose when logging data, if set to a [[true value|00001006030500]].

  Default: ""false""

Added docs/manual/00001004010200.zettel.



























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
id: 00001004010200
title: Zettelstore runtime profiling
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20240926144556
modified: 20240926144951

For debugging purposes, you can enable runtime profiling by setting the startup configuration [[''runtime-profiling''|00001004010000#runtime-profiling]].
Typically, a Zettelstore developer will do this.
In certain cases, a Zettelstore developer will ask you to enable runtime profiling, because you encountered a hard error.

Runtime profiling will generate some data that can be retrieved through the builtin web server.
The following URL paths are valid:

|=Path|Description
|''/rtp/''|Show an index page, where you can navigate to detailed information
|''/rtp/allocs''|Show a sampling of all past memory allocations
|''/rtp/block''|Show stack traces that led to internal blocking
|''/rtp/cmdline''|Show the running Zettelstore command line, with arguments separated by NUL bytes
|''/rtp/goroutine''|Show stack traces of all current internal activities
|''/rtp/heap''|Show a sampling of memory allocations of live objects
|''/rtp/mutex''|Show stack traces of holders of contended mutexes
|''/rtp/profile''|Execute a CPU profile
|''/rtp/symbol''|Shows function names for given program counter value
|''/rtp/trace''|Show trace of execution of the current program
|''/rtp/threadcreate''|Show stack traces that led to the creation of new OS threads

See documentation for Go standard package [[''net/http/pprof''|https://pkg.go.dev/net/http/pprof]].

Changes to docs/manual/00001004011200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004011200
title: Zettelstore boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220307121547

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

An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004011200
title: Zettelstore boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220307121547

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

An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore.

Changes to docs/manual/00001004011400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220724200512

Under certain circumstances, it is preferable to further configure a file directory box.
This is done by appending query parameters after the base box URI ''dir:\//DIR''.

The following parameters are supported:

|= Parameter:|Description|Default value:|





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240710180215

Under certain circumstances, it is preferable to further configure a file directory box.
This is done by appending query parameters after the base box URI ''dir:\//DIR''.

The following parameters are supported:

|= Parameter:|Description|Default value:|
52
53
54
55
56
57
58
59

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







|
53
54
55
56
57
58
59
60

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

Changes to docs/manual/00001004011600.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004011600
title: Configure memory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220307122554

Under most circumstances, it is preferable to further configure a memory box.
This is done by appending query parameters after the base box URI ''mem:''.

The following parameters are supported:






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004011600
title: Configure memory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20220307112918
modified: 20220307122554

Under most circumstances, it is preferable to further configure a memory box.
This is done by appending query parameters after the base box URI ''mem:''.

The following parameters are supported:

Changes to docs/manual/00001004020000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231126180829
show-back-links: false

You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called __configuration zettel__.
The following metadata keys change the appearance / behavior of Zettelstore.
Some of them can be overwritten in an [[user zettel|00001010040200]], a subset of those may be overwritten in zettel that is currently used.
See the full list of [[metadata that may be overwritten|00001004020200]].






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241118175216
show-back-links: false

You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called __configuration zettel__.
The following metadata keys change the appearance / behavior of Zettelstore.
Some of them can be overwritten in an [[user zettel|00001010040200]], a subset of those may be overwritten in zettel that is currently used.
See the full list of [[metadata that may be overwritten|00001004020200]].
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

  Default: ""1024"".
; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-subordinate-links|''show-subordinate-links''], [!show-successor-links|''show-successor-links'']
: When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel.
  This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''subordinates''|00001006020000#subordinates]], and  [[''successors''|00001006020000#successors]].

  These configuration keys may be used to show, not to show, or to close the list of referenced zettel.

  Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list).

  Default: """".








|

|







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

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

  Default: ""1024"".
; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-sequel-links|''show-sequel-links''], [!show-successor-links|''show-successor-links'']
: When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel.
  This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''sequel''|00001006020000#sequel]], and  [[''prequel''|00001006020000#prequel]].

  These configuration keys may be used to show, not to show, or to close the list of referenced zettel.

  Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list).

  Default: """".

Changes to docs/manual/00001004020200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001004020200
title: Runtime configuration data that may be user specific or zettel specific
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20221205155521
modified: 20231126180752

Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]].
A subset of those may be overwritten in zettel that is currently used.
This allows to specify user specific or zettel specific behavior.

The following metadata keys are supported to provide a more specific behavior:

|=Key|User:|Zettel:|Remarks
|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N|
|[[''home-zettel''|00001004020000#home-zettel]]|Y|N|
|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful
|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y|
|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y|
|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y|
|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y|






|













|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001004020200
title: Runtime configuration data that may be user specific or zettel specific
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20221205155521
modified: 20241118175124

Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]].
A subset of those may be overwritten in zettel that is currently used.
This allows to specify user specific or zettel specific behavior.

The following metadata keys are supported to provide a more specific behavior:

|=Key|User:|Zettel:|Remarks
|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N|
|[[''home-zettel''|00001004020000#home-zettel]]|Y|N|
|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful
|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y|
|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y|
|[[''show-sequel-links''|00001004020000#show-sequel-links]]|Y|Y|
|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y|

Changes to docs/manual/00001004050200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20210712233414

Lists all implemented sub-commands.

Example:
```
# zettelstore help





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210712233414

Lists all implemented sub-commands.

Example:
```
# zettelstore help

Changes to docs/manual/00001004050400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20211124182041

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

The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124182041

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

The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer.

Changes to docs/manual/00001004051000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20220724162050

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

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





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220724162050

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

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

Changes to docs/manual/00001004051400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20210712234305

This sub-command is used to create a hashed password for to be authenticated users.

It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output.

The general usage is:





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210712234305

This sub-command is used to create a hashed password for to be authenticated users.

It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output.

The general usage is:

Changes to docs/manual/00001004059900.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004059900
title: Command line flags for profiling the application
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20211122174951

If you want to measure potential bottlenecks within the software Zettelstore,
there are two [[command line|00001004050000]] flags for enabling the measurement (also called __profiling__):

; ''-cpuprofile FILE''
: Enables CPU profiling.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004059900
title: Command line flags for profiling the application
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20211122170506
modified: 20211122174951

If you want to measure potential bottlenecks within the software Zettelstore,
there are two [[command line|00001004050000]] flags for enabling the measurement (also called __profiling__):

; ''-cpuprofile FILE''
: Enables CPU profiling.

Changes to docs/manual/00001004100000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004100000
title: Zettelstore Administrator Console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20211103162926

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

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






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004100000
title: Zettelstore Administrator Console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210510141304
modified: 20211103162926

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

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

Changes to docs/manual/00001004101000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004101000
title: List of supported commands of the administrator console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220823194553

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






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004101000
title: List of supported commands of the administrator console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210510141304
modified: 20220823194553

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

Changes to docs/manual/00001005000000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk

modified: 20220104213511

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

Zettelstore provides additional services to the user.
Via the builtin [[web user interface|00001014000000]] you can work with zettel in various ways.
For example, you are able to list zettel, to create new zettel, to edit them, or to delete them.
You can view zettel details and relations between zettel.

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

=== Where zettel are stored

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

Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]].
If you create a new zettel via the [[web user interface|00001014000000]] or via the [[API|00001012053200]], the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss'').
This allows zettel to be sorted naturally by creation time.

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

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

Two filename extensions are used by Zettelstore:
# ''.zettel'' is a format that stores metadata and content together in one file,
# the empty file extension is used, when the content must be stored in its own file, e.g. image data;
  in this case, the filename just the 14 digits of the zettel identifier, and optional characters except the period ''"."''.  
Other filename extensions are used to determine the ""syntax"" of a zettel.
This allows to use other content within the Zettelstore, e.g. images or HTML templates.

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

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

=== Predefined zettel

Zettelstore contains some [[predefined zettel|00001005090000]] to work properly.





>
|













|














|















|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
id: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241213101751

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

Zettelstore provides additional services to the user.
Via the builtin [[web user interface|00001014000000]] you can work with zettel in various ways.
For example, you are able to list zettel, to create new zettel, to edit them, or to delete them.
You can view zettel details and relations between zettel.

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

=== Where zettel are stored

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

Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]].
If you create a new zettel via the [[web user interface|00001014000000]] or via the [[API|00001012053200]], the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss'').
This allows zettel to be sorted naturally by creation time.

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

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

Two filename extensions are used by Zettelstore:
# ''.zettel'' is a format that stores metadata and content together in one file,
# the empty file extension is used, when the content must be stored in its own file, e.g. image data;
  in this case, the filename just the 14 digits of the zettel identifier, and optional characters except the period ''"."''.  
Other filename extensions are used to determine the ""syntax"" of a zettel.
This allows to use other content within the Zettelstore, e.g. images or HTML templates.

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

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

=== Predefined zettel

Zettelstore contains some [[predefined zettel|00001005090000]] to work properly.
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together.
If you change a zettel, it will be always stored as a file.
If a zettel is requested, Zettelstore will first try to read that zettel from a file.
If such a file was not found, the internal zettel store is searched secondly.

Therefore, the file store ""shadows"" the internal zettel store.
If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier.
Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software.

* [[List of predefined zettel|00001005090000]]

=== Boxes: alternative ways to store zettel
As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself.
Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.]







|







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together.
If you change a zettel, it will be always stored as a file.
If a zettel is requested, Zettelstore will first try to read that zettel from a file.
If such a file was not found, the internal zettel store is searched secondly.

Therefore, the file store ""shadows"" the internal zettel store.
If you want to read the original zettel, you have to delete the zettel (which removes it from the file directory).
Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software.

* [[List of predefined zettel|00001005090000]]

=== Boxes: alternative ways to store zettel
As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself.
Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.]

Changes to docs/manual/00001005090000.zettel.

1
2
3
4
5
6
7
8
9




10
11
12
13
14
15
16
17
18


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

42
43
44
45
46

47
48
49

50
51
id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231129173425

The following table lists all predefined zettel with their purpose.





|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore
| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore
| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content
| [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages


| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore
| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]
| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]]
| [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel
| [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel
| [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel
| [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text
| [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]]
| [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel
| [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message
| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates
| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid
| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]""
| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]""
| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]""
| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]""

| [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]
| [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]]
| [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]]

| [[00010000000000]] | Home | Default home zettel, contains some welcome information

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


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






|


>
>
>
>









>
>











<











>





>



>


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

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241223214236

The following table lists all predefined zettel with their purpose.

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

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore
| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore
| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content
| [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages
| [[00000000000008]] | Zettelstore Memory | Some statistics about main memory usage
| [[00000000000009]] | Zettelstore Sx Engine | Statistics about the [[Sx|https://t73f.de/r/sx]] engine, which interprets symbolic expressions
| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore
| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]
| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]]
| [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel
| [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel
| [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel
| [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text

| [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel
| [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message
| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates
| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid
| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]""
| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]""
| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]""
| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]""
| [[00000000080001]] | Lists Menu | Contains the items of the ""Lists"" menu
| [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]
| [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]]
| [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]]
| [[00000999999999]] | Zettelstore Application Directory | Maps application name to application specific zettel
| [[00010000000000]] | Home | Default home zettel, contains some welcome information

If a zettel is not linked, it is not accessible for the current user.
In most cases, you must at least enable [[''expert-mode''|00001004020000#expert-mode]].

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

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230704161159

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

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

; [!author|''author'']






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241118175033

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

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

; [!author|''author'']
33
34
35
36
37
38
39






40
41
42
43
44
45
46
  This is a computed value.
  There is no need to set it via Zettelstore.
  
  If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used.

  Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second.
  When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible.






; [!credential|''credential'']
: Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]].
  It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key.

  It is only used for zettel with a ''role'' value of ""user"".
; [!dead|''dead'']
: Property that contains all references that does __not__ identify a zettel.







>
>
>
>
>
>







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  This is a computed value.
  There is no need to set it via Zettelstore.
  
  If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used.

  Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second.
  When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible.
; [!created-missing|''created-missing'']
: If set to ""true"", the value of [[''created''|#created]] was not stored within a zettel.
  To allow the migration of [[zettel identifier|00001006050000]] to a new scheme, you should update the value of ''created'' to a reasonable value.
  Otherwise you might lose that information in future releases.

  This key will be removed when the migration to a new zettel identifier format has been completed.
; [!credential|''credential'']
: Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]].
  It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key.

  It is only used for zettel with a ''role'' value of ""user"".
; [!dead|''dead'']
: Property that contains all references that does __not__ identify a zettel.
79
80
81
82
83
84
85



86
87
88
89
90
91
92
; [!precursor|''precursor'']
: References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel.
  Basically the inverse of key [[''folge''|#folge]].
; [!predecessor|''predecessor'']
: References the zettel that contains a previous version of the content.
  In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons.
  Basically the inverse of key [[''successors''|#successors]].



; [!published|''published'']
: This property contains the timestamp of the mast modification / creation of the zettel.
  If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value.
  Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value.
  Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used.
  In all other cases, this property is not set.








>
>
>







85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
; [!precursor|''precursor'']
: References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel.
  Basically the inverse of key [[''folge''|#folge]].
; [!predecessor|''predecessor'']
: References the zettel that contains a previous version of the content.
  In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons.
  Basically the inverse of key [[''successors''|#successors]].
; [!prequel|''prequel'']
: Specifies a zettel that is conceptually a prequel zettel.
  This is a zettel that occured somehow before the current zettel.
; [!published|''published'']
: This property contains the timestamp of the mast modification / creation of the zettel.
  If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value.
  Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value.
  Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used.
  In all other cases, this property is not set.

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
: Marks a zettel as read-only.
  The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not.
; [!role|''role'']
: Defines the role of the zettel.
  Can be used for selecting zettel.
  See [[supported zettel roles|00001006020100]].
  If not given, it is ignored.
; [!subordinates|''subordinates'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value.
; [!successors|''successors'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value.
  Therefore, it references all zettel that contain a new version of the content and/or metadata.
  In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons.
  In most cases, zettel referencing the current zettel should be updated to reference a successor zettel.
  The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel.
; [!summary|''summary'']
: Summarizes the content of the zettel.
  You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup.
; [!superior|''superior'']
: Specifies a zettel that is conceptually a superior zettel.
  This might be a more abstract zettel, or a zettel that should be higher in a hierarchy.
; [!syntax|''syntax'']
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If it is not given, it defaults to ''plain''.
; [!tags|''tags'']
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must begin with the number sign character (""''#''"", U+0023).
; [!title|''title'']
: Specifies the title of the zettel.
  If not given, the value of [[''id''|#id]] will be used.
; [!url|''url'']
: Defines an URL / URI for this zettel that possibly references external material.
  One use case is to specify the document that the current zettel comments on.
  The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template.
; [!useless-files|''useless-files'']
: Contains the file names that are rejected to serve the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].
  If a zettel is renamed or deleted, these files will be deleted.
; [!user-id|''user-id'']
: Provides some unique user identification for an [[user zettel|00001010040200]].
  It is used as a user name for authentication.

  It is only used for zettel with a ''role'' value of ""user"".
; [!user-role|''user-role'']
: Defines the basic privileges of an authenticated user, e.g. reading / changing zettel.







|
|









<
<
<

















|







110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
: Marks a zettel as read-only.
  The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not.
; [!role|''role'']
: Defines the role of the zettel.
  Can be used for selecting zettel.
  See [[supported zettel roles|00001006020100]].
  If not given, it is ignored.
; [!sequel|''sequel'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] value.
; [!successors|''successors'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value.
  Therefore, it references all zettel that contain a new version of the content and/or metadata.
  In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons.
  In most cases, zettel referencing the current zettel should be updated to reference a successor zettel.
  The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel.
; [!summary|''summary'']
: Summarizes the content of the zettel.
  You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup.



; [!syntax|''syntax'']
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If it is not given, it defaults to ''plain''.
; [!tags|''tags'']
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must begin with the number sign character (""''#''"", U+0023).
; [!title|''title'']
: Specifies the title of the zettel.
  If not given, the value of [[''id''|#id]] will be used.
; [!url|''url'']
: Defines an URL / URI for this zettel that possibly references external material.
  One use case is to specify the document that the current zettel comments on.
  The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template.
; [!useless-files|''useless-files'']
: Contains the file names that are rejected to serve the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].
  If a zettel is deleted, these files will also be deleted.
; [!user-id|''user-id'']
: Provides some unique user identification for an [[user zettel|00001010040200]].
  It is used as a user name for authentication.

  It is only used for zettel with a ''role'' value of ""user"".
; [!user-role|''user-role'']
: Defines the basic privileges of an authenticated user, e.g. reading / changing zettel.

Changes to docs/manual/00001006020400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

modified: 20211124132040

A zettel can be marked as read-only, if it contains a metadata value for key
[[''read-only''|00001006020000#read-only]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel,
depending on their [[user role|00001010070300]].
Otherwise, the read-only mark is just a binary value.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124132040

A zettel can be marked as read-only, if it contains a metadata value for key
[[''read-only''|00001006020000#read-only]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel,
depending on their [[user role|00001010070300]].
Otherwise, the read-only mark is just a binary value.

Changes to docs/manual/00001006030500.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001006030500
title: Boolean Value
role: manual
tags: #manual #reference #zettel #zettelstore
syntax: zmk

modified: 20220304114040

On some places, metadata values are interpreted as a truth value.

Every character sequence that begins with a ""0"", ""F"", ""N"", ""f"", or a ""n"" is interpreted as the boolean ""false"" value.
All values are interpreted as the boolean ""true"" value.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001006030500
title: Boolean Value
role: manual
tags: #manual #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220304114040

On some places, metadata values are interpreted as a truth value.

Every character sequence that begins with a ""0"", ""F"", ""N"", ""f"", or a ""n"" is interpreted as the boolean ""false"" value.
All values are interpreted as the boolean ""true"" value.

Changes to docs/manual/00001006050000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001006050000
title: Zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk

modified: 20210721123222

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006050000
title: Zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241128141443

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the

Changes to docs/manual/00001006055000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
id: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk

modified: 20220311111751

[[Zettel identifier|00001006050000]] are typically created by examine the current date and time.
By renaming a zettel, you are able to provide any sequence of 14 digits.
If no other zettel has the same identifier, you are allowed to rename a zettel.

To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000'').

All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.].
Zettel identifier of this manual have be chosen to begin with ''000010''.

However, some external applications may need a range of specific zettel identifier to work properly.
Identifier that begin with ''00009'' can be used for such purpose.
To request a reservation, please send an email to the maintainer of Zettelstore.
The request must include the following data:
; Title
: Title of you application
; Description
: A brief description what the application is used for and why you need to reserve some zettel identifier
; Number
: Specify the amount of zettel identifier you are planning to use.
  Minimum size is 100.
  If you need more than 10.000, your justification will contain more words.

=== Reserved Zettel Identifier

|= From | To | Description
| 00000000000000 | 0000000000000 | This is an invalid zettel identifier
| 00000000000001 | 0000009999999 | [[Predefined zettel|00001005090000]]
| 00000100000000 | 0000019999999 | This [[Zettelstore manual|00001000000000]]
| 00000200000000 | 0000899999999 | Reserved for future use
| 00009000000000 | 0000999999999 | Reserved for applications

This list may change in the future.

==== External Applications
|= From | To | Description
| 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow
| 00009000002000 | 00009000002999 | [[Zettel Blog|https://zettelstore.de/contrib]], an application to collect and transform zettel into a blog





>
|


|
<

|




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




|
|
|
|
|
<
<




<
1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17




18

19

20


21
22
23
24
25
26
27
28
29


30
31
32
33

id: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210721105704
modified: 20241202100917

[[Zettel identifier|00001006050000]] are typically created by examine the current date and time.
By renaming the name of the underlying zettel file, you are able to provide any sequence of 14 digits.


To make things easier, you must not use zettel identifier that begin with four zeroes (''0000'').

All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.].
Zettel identifier of this manual have be chosen to begin with ''000010''.

However, some external applications may need at least one defined zettel identifier to work properly.




Zettel [[Zettelstore Application Directory|00000999999999]] (''00000999999999'') can be used to associate a name to a zettel identifier.

For example, if your application is named ""app"", you create a metadata key ''app-zid''.

Its value is the zettel identifier of the zettel that configures your application.



=== Reserved Zettel Identifier

|= From | To | Description
| 00000000000000 | 00000000000000 | This is an invalid zettel identifier
| 00000000000001 | 00000999999999 | [[Predefined zettel|00001005090000]]
| 00001000000000 | 00001099999999 | This [[Zettelstore manual|00001000000000]]
| 00001100000000 | 00008999999999 | Reserved, do not use
| 00009000000000 | 00009999999999 | Reserved for applications



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

Changes to docs/manual/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
id: 00001007000000
title: Zettelmarkup
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221209192105

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

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

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







|








|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001007000000
title: Zettelmarkup
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241212152823

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

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

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

Changes to docs/manual/00001007010000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007010000
title: Zettelmarkup: General Principles
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20211124175047

Any document can be thought as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks.
Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs.
Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images.

With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters.
List blocks also begin at the first position of a line, but may need one or more identical character, plus a space character.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007010000
title: Zettelmarkup: General Principles
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241213101524

Any document can be thought as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks.
Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs.
Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images.

With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters.
List blocks also begin at the first position of a line, but may need one or more identical character, plus a space character.
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

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

To summarize:

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

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







|










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

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

To summarize:

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

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

Changes to docs/manual/00001007020000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007020000
title: Zettelmarkup: Basic Definitions
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218130713

Every Zettelmarkup content consists of a sequence of Unicode code-points.
Unicode code-points are called in the following as **character**s.

Characters are encoded with UTF-8.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007020000
title: Zettelmarkup: Basic Definitions
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218130713

Every Zettelmarkup content consists of a sequence of Unicode code-points.
Unicode code-points are called in the following as **character**s.

Characters are encoded with UTF-8.

Changes to docs/manual/00001007030000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007030000
title: Zettelmarkup: Block-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311181036

Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists






>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007030000
title: Zettelmarkup: Block-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241212153023

Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists

61
62
63
64
65
66
67
68
69
70
71
72
73
74
:::example
= Heading
Some text follows.
:::
This is because headings need at least three equal sign character.

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

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

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







|






62
63
64
65
66
67
68
69
70
71
72
73
74
75
:::example
= Heading
Some text follows.
:::
This is because headings need at least three equal sign character.

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

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

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

Changes to docs/manual/00001007030100.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030100
title: Zettelmarkup: Description Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131155

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

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





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030100
title: Zettelmarkup: Description Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131155

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

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

Changes to docs/manual/00001007030200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001007030200
title: Zettelmarkup: Nested Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133902

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

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

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

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

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





>
|

|

|








|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
id: 00001007030200
title: Zettelmarkup: Nested Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241213121000

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

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

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

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

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

Changes to docs/manual/00001007030300.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030300
title: Zettelmarkup: Headings
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133755

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

```zmk





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030300
title: Zettelmarkup: Headings
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218133755

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

```zmk

Changes to docs/manual/00001007030500.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030500
title: Zettelmarkup: Verbatim Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131500

Verbatim blocks are used to enter text that should not be interpreted.
They begin with at least three grave accent characters (""''`''"", U+0060) at the first position of a line.
Alternatively, a modifier letter grave accent (""''Ë‹''"", U+02CB) is also allowed[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.].

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030500
title: Zettelmarkup: Verbatim Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131500

Verbatim blocks are used to enter text that should not be interpreted.
They begin with at least three grave accent characters (""''`''"", U+0060) at the first position of a line.
Alternatively, a modifier letter grave accent (""''Ë‹''"", U+02CB) is also allowed[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.].

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.

Changes to docs/manual/00001007030600.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030600
title: Zettelmarkup: Quotation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131806

A simple way to enter a quotation is to use the [[quotation list|00001007030200]].
A quotation list loosely follows the convention of quoting text within emails.
However, if you want to attribute the quotation to someone, a quotation block is more appropriately.

This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030600
title: Zettelmarkup: Quotation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131806

A simple way to enter a quotation is to use the [[quotation list|00001007030200]].
A quotation list loosely follows the convention of quoting text within emails.
However, if you want to attribute the quotation to someone, a quotation block is more appropriately.

This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line.

Changes to docs/manual/00001007030700.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030700
title: Zettelmarkup: Verse Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218132432

Sometimes, you want to enter text with significant space characters at the beginning of each line and with significant line endings.
Poetry is one typical example.
Of course, you could help yourself with hard space characters and hard line breaks, by entering a backslash character before a space character and at the end of each line.
Using a verse block might be easier.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030700
title: Zettelmarkup: Verse Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218132432

Sometimes, you want to enter text with significant space characters at the beginning of each line and with significant line endings.
Poetry is one typical example.
Of course, you could help yourself with hard space characters and hard line breaks, by entering a backslash character before a space character and at the end of each line.
Using a verse block might be easier.

Changes to docs/manual/00001007030800.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030800
title: Zettelmarkup: Region Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220323190829

Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030800
title: Zettelmarkup: Region Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220323190829

Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.

Changes to docs/manual/00001007031000.zettel.

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

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

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.





>
|


|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001007031000
title: Zettelmarkup: Tables
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241212153641

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

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

Changes to docs/manual/00001007031140.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20240219161800

A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''.
The line must literally start with the sequence ''{{{query:''.
Everything after this prefix is interpreted as a [[query expression|00001007700000]].

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20241213153229

A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''.
The line must literally start with the sequence ''{{{query:''.
Everything after this prefix is interpreted as a [[query expression|00001007700000]].

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
: The resulting list will be a numbered list.
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''TITLE'' (parameter)
: All words following ''TITLE'' are joined together to form a title.
  It is used for the ''ATOM'' and ''RSS'' action.
; ''ATOM'' (aggregate)
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'', ''REINDEX'' (aggregate)
: Will be ignored.
  These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful).
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.







<
<
<
<
<
<
<
<
<







34
35
36
37
38
39
40









41
42
43
44
45
46
47
: The resulting list will be a numbered list.
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.









; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'', ''REINDEX'' (aggregate)
: Will be ignored.
  These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful).
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.

Changes to docs/manual/00001007040200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040200
title: Zettelmarkup: Literal-like formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311185110

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.
* Mark text as input you give into a computer via a keyboard.
* Mark text as output from some computer, e.g. shown at the command line.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040200
title: Zettelmarkup: Literal-like formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220311185110

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.
* Mark text as input you give into a computer via a keyboard.
* Mark text as output from some computer, e.g. shown at the command line.

Changes to docs/manual/00001007040300.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040300
title: Zettelmarkup: Reference-like text
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20210810172531

An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material.

There are several kinds of references that are allowed in Zettelmarkup:
* [[Links to other zettel or to (external) material|00001007040310]]
* [[Embedded zettel or (external) material|00001007040320]] (""inline transclusion"")





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040300
title: Zettelmarkup: Reference-like text
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210810172531

An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material.

There are several kinds of references that are allowed in Zettelmarkup:
* [[Links to other zettel or to (external) material|00001007040310]]
* [[Embedded zettel or (external) material|00001007040320]] (""inline transclusion"")

Changes to docs/manual/00001007040322.zettel.

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

28
id: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20221112111054

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

Supported formats are:

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

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

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

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

** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}.






|



















|
>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
id: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20241202101206

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

Supported formats are:

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

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

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

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

Changes to docs/manual/00001007040330.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040330
title: Zettelmarkup: Footnotes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218130100

A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", U+005E), followed by some text, and ends with a right square bracket.

Example:

``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040330
title: Zettelmarkup: Footnotes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20220218130100

A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", U+005E), followed by some text, and ends with a right square bracket.

Example:

``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}.

Changes to docs/manual/00001007040340.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040340
title: Zettelmarkup: Citation Key
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133447

A citation key references some external material that is part of a bibliographical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040340
title: Zettelmarkup: Citation Key
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20220218133447

A citation key references some external material that is part of a bibliographical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given.

Changes to docs/manual/00001007040350.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040350
title: Zettelmarkup: Mark
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133206

A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a zettel, either with a [[link|00001007040310]] or with an [[inline-mode transclusion|00001007040324]].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", U+0021).
Now the optional mark name follows.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040350
title: Zettelmarkup: Mark
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20220218133206

A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a zettel, either with a [[link|00001007040310]] or with an [[inline-mode transclusion|00001007040324]].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", U+0021).
Now the optional mark name follows.

Changes to docs/manual/00001007050000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007050000
title: Zettelmarkup: Attributes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220630194106

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

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





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007050000
title: Zettelmarkup: Attributes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220630194106

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

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

Changes to docs/manual/00001007050200.zettel.

1
2
3
4
5

6
7
id: 00001007050200
title: Zettelmarkup: Supported Attribute Values for Programming Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual


TBD





>


1
2
3
4
5
6
7
8
id: 00001007050200
title: Zettelmarkup: Supported Attribute Values for Programming Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual
created: 20210126175322

TBD

Changes to docs/manual/00001007706000.zettel.

1
2
3
4
5

6
7
8
9
10
id: 00001007706000
title: Search value
role: manual
tags: #manual #search #zettelstore
syntax: zmk

modified: 20220807162031

A search value specifies a value to be searched for, depending on the [[search operator|00001007705000]].

A search value should be lower case, because all comparisons are done in a case-insensitive way and there are some upper case keywords planned.





>





1
2
3
4
5
6
7
8
9
10
11
id: 00001007706000
title: Search value
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20220807162031

A search value specifies a value to be searched for, depending on the [[search operator|00001007705000]].

A search value should be lower case, because all comparisons are done in a case-insensitive way and there are some upper case keywords planned.

Changes to docs/manual/00001007720300.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
id: 00001007720300
title: Query: Context Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707204706
modified: 20240209191045

A context directive calculates the __context__ of a list of zettel identifier.
It starts with the keyword ''CONTEXT''.

Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters.
These are:
* ''FULL'': additionally search for zettel with the same tags,
* ''BACKWARD'': search for context only though backward links,
* ''FORWARD'': search for context only through forward links,
* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17),
* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200).

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

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







|
















|
|







1
2
3
4
5
6
7
8
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: 00001007720300
title: Query: Context Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707204706
modified: 20241118174741

A context directive calculates the __context__ of a list of zettel identifier.
It starts with the keyword ''CONTEXT''.

Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters.
These are:
* ''FULL'': additionally search for zettel with the same tags,
* ''BACKWARD'': search for context only though backward links,
* ''FORWARD'': search for context only through forward links,
* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17),
* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200).

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

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

Changes to docs/manual/00001007720900.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007720900
title: Query: Items Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 00010101000000
modified: 20230729120755

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

What is a ""table of contents""?





|







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007720900
title: Query: Items Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230729102142
modified: 20230729120755

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

What is a ""table of contents""?

Changes to docs/manual/00001007780000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20240219155949

```
QueryExpression   := ZettelList? QueryDirective* SearchExpression ActionExpression?
ZettelList        := (ZID (SPACE+ ZID)*).
ZID               := '0'+ ('1' .. '9'') DIGIT*
                   | ('1' .. '9') DIGIT*.
QueryDirective    := ContextDirective






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20241213153200

```
QueryExpression   := ZettelList? QueryDirective* SearchExpression ActionExpression?
ZettelList        := (ZID (SPACE+ ZID)*).
ZID               := '0'+ ('1' .. '9'') DIGIT*
                   | ('1' .. '9') DIGIT*.
QueryDirective    := ContextDirective
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
                   | ('!')? ('~' | ':' | '[' | '}').
ExistOperator     := '?'
                   | '!' '?'.
PosInt            := '0'
                   | ('1' .. '9') DIGIT*.
ActionExpression  := '|' (Word (SPACE+ Word)*)?
Action            := Word
                   | 'ATOM'
                   | 'KEYS'
                   | 'N' NO-SPACE*
                   | 'MAX' PosInt
                   | 'MIN' PosInt
                   | 'REDIRECT'
                   | 'REINDEX'
                   | 'RSS'
                   | 'TITLE' (SPACE Word)* .
Word              := NO-SPACE NO-SPACE*
```







<





|
<
<


40
41
42
43
44
45
46

47
48
49
50
51
52


53
54
                   | ('!')? ('~' | ':' | '[' | '}').
ExistOperator     := '?'
                   | '!' '?'.
PosInt            := '0'
                   | ('1' .. '9') DIGIT*.
ActionExpression  := '|' (Word (SPACE+ Word)*)?
Action            := Word

                   | 'KEYS'
                   | 'N' NO-SPACE*
                   | 'MAX' PosInt
                   | 'MIN' PosInt
                   | 'REDIRECT'
                   | 'REINDEX'.


Word              := NO-SPACE NO-SPACE*
```

Changes to docs/manual/00001007800000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
id: 00001007800000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
created: 20220805150154
modified: 20231113191330

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

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





|
|














|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
id: 00001007800000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241125182149

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

|= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[marked / highlighted text|00001007040100]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]
| ''&''  | (free) | [[Entity|00001007040000]]
| ''\'''  | (free)  | [[Computer input|00001007040200]]
| ''(''  | (free) | (free)
| '')''  | (free) | (free)
| ''*''  | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]]
| ''+''  | (reserved) | (reserved)
| '',''  | (free) | [[Sub-scripted text|00001007040100]]
| ''-''  | [[Horizontal rule|00001007030400]] | ""[[en-dash|00001007040000]]""
| ''.''  | (free) | (free)
| ''/''  | (free) | (free)
| '':''  | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]]
| '';''  | [[Description term|00001007030100]] | (free)
| ''<''  | [[Quotation block|00001007030600]] | (free)

Changes to docs/manual/00001008000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210126175300
modified: 20230529223634

[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* CSS






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210126175300
modified: 20240413160242

[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* CSS
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
: Only the metadata of a zettel is ""parsed"".
  Useful for displaying the full metadata.
  The [[runtime configuration zettel|00000000000100]] uses this syntax.
  The zettel content is ignored.
; [!svg|''svg'']
: [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]].
; [!sxn|''sxn'']
: S-Expressions, as implemented by [[sx|https://zettelstore.de/sx]].
  Often used to specify templates when rendering a zettel as HTML for the [[web user interface|00001014000000]] (with the help of sxhtml]).
; [!text|''text''], [!plain|''plain''], [!txt|''txt'']
: Plain text that must not be interpreted further.
; [!zmk|''zmk'']
: [[Zettelmarkup|00001007000000]].

The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]].

If you specify something else, your content will be interpreted as plain text.







|









45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
: Only the metadata of a zettel is ""parsed"".
  Useful for displaying the full metadata.
  The [[runtime configuration zettel|00000000000100]] uses this syntax.
  The zettel content is ignored.
; [!svg|''svg'']
: [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]].
; [!sxn|''sxn'']
: S-Expressions, as implemented by [[Sx|https://t73f.de/r/sx]].
  Often used to specify templates when rendering a zettel as HTML for the [[web user interface|00001014000000]] (with the help of sxhtml]).
; [!text|''text''], [!plain|''plain''], [!txt|''txt'']
: Plain text that must not be interpreted further.
; [!zmk|''zmk'']
: [[Zettelmarkup|00001007000000]].

The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]].

If you specify something else, your content will be interpreted as plain text.

Changes to docs/manual/00001010000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
id: 00001010000000
title: Security
role: manual
tags: #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221018123622

Your zettel could contain sensitive content.
You probably want to ensure that only authorized person can read and/or modify them.
Zettelstore ensures this in various ways.

=== Local first
The Zettelstore is designed to run on your local computer.
If you do not configure it in other ways, no person from another computer can connect to your Zettelstore.
You must explicitly configure it to allow access from other computers.

In the case that you own multiple computers, you do not have to access the Zettelstore remotely.
You could install Zettelstore on each computer and set-up some software to synchronize your zettel.
Since zettel are stored as ordinary files, this task could be done in various ways.

=== Read-only
You can start the Zettelstore in an read-only mode.
Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.].

You enable read-only mode through the key ''readonly'' in the [[startup configuration zettel|00001004010000#readonly]] or with the ''-r'' option of the ``zettelstore run`` sub-command.

=== Authentication
The Zettelstore can be configured that a user must authenticate itself to gain access to the content.

* [[How to enable authentication|00001010040100]]
* [[How to add a new user|00001010040200]]
* [[How users are authenticated|00001010040400]] (some technical background)
* [[Authenticated sessions|00001010040700]]

=== Authorization






|

|
|












|





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
id: 00001010000000
title: Security
role: manual
tags: #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241213102811

Your zettel may contain sensitive content.
You probably want to ensure that only authorized persons can read and/or modify them.
Zettelstore ensures this in various ways.

=== Local first
The Zettelstore is designed to run on your local computer.
If you do not configure it in other ways, no person from another computer can connect to your Zettelstore.
You must explicitly configure it to allow access from other computers.

In the case that you own multiple computers, you do not have to access the Zettelstore remotely.
You could install Zettelstore on each computer and set-up some software to synchronize your zettel.
Since zettel are stored as ordinary files, this task could be done in various ways.

=== Read-only
You can start the Zettelstore in a read-only mode.
Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.].

You enable read-only mode through the key ''readonly'' in the [[startup configuration zettel|00001004010000#readonly]] or with the ''-r'' option of the ``zettelstore run`` sub-command.

=== Authentication
The Zettelstore can be configured that users must authenticate themselves to gain access to the content.

* [[How to enable authentication|00001010040100]]
* [[How to add a new user|00001010040200]]
* [[How users are authenticated|00001010040400]] (some technical background)
* [[Authenticated sessions|00001010040700]]

=== Authorization

Changes to docs/manual/00001010040100.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001010040100
title: Enable authentication
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk

modified: 20220419192817

To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner.
Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''.
Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled.

Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to some random string data (minimum length is 16 bytes) to secure the data exchanged with a client system.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001010040100
title: Enable authentication
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220419192817

To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner.
Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''.
Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled.

Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to some random string data (minimum length is 16 bytes) to secure the data exchanged with a client system.

Changes to docs/manual/00001010040400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001010040400
title: Authentication process
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk

modified: 20211127174943

When someone tries to authenticate itself with an user identifier / ""user name"" and a password, the following process is executed:

# If meta key ''owner'' of the configuration zettel does not have a valid [[zettel identifier|00001006050000]] as value, authentication fails.
# Retrieve all zettel, where the meta key ''user-id'' has the same value as the given user identification. If the list is empty, authentication fails.
# From above list, the zettel with the numerically smallest identifier is selected.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001010040400
title: Authentication process
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211127174943

When someone tries to authenticate itself with an user identifier / ""user name"" and a password, the following process is executed:

# If meta key ''owner'' of the configuration zettel does not have a valid [[zettel identifier|00001006050000]] as value, authentication fails.
# Retrieve all zettel, where the meta key ''user-id'' has the same value as the given user identification. If the list is empty, authentication fails.
# From above list, the zettel with the numerically smallest identifier is selected.

Changes to docs/manual/00001010040700.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
id: 00001010040700
title: Access token
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk

modified: 20211202120950

If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller.
Otherwise the user will not be recognized by Zettelstore.

If the user was authenticated via the [[web user interface|00001014000000]], the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]].
When the web browser is closed, theses cookies are not saved.
If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''.

If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime.
The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration.
Every time a web page is displayed, a fresh token is created and stored inside the cookie.

If the user was authenticated via the API, the access token will be returned as the content of the response.
Typically, the lifetime of this token is more short term, e.g. 10 minutes.
It is specified by the ''token-lifetime-api'' value of the startup configuration.
If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]].

If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), your must set the ''insecure-cookie'' value of the startup configuration to ''true''.
In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text.
You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore.





>
|





|











|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001010040700
title: Access token
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241213101607

If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller.
Otherwise the user will not be recognized by Zettelstore.

If the user was authenticated via the [[web user interface|00001014000000]], the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]].
When the web browser is closed, these cookies are not saved.
If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''.

If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime.
The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration.
Every time a web page is displayed, a fresh token is created and stored inside the cookie.

If the user was authenticated via the API, the access token will be returned as the content of the response.
Typically, the lifetime of this token is more short term, e.g. 10 minutes.
It is specified by the ''token-lifetime-api'' value of the startup configuration.
If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]].

If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), you must set the ''insecure-cookie'' value of the startup configuration to ''true''.
In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text.
You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore.

Changes to docs/manual/00001010070300.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001010070300
title: User roles
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk

modified: 20220214175212

Every user is associated with some basic privileges.
These are specified in the [[user zettel|00001010040200]] with the key ''user-role''.
The following values are supported:

; [!reader|""reader""]





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001010070300
title: User roles
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220214175212

Every user is associated with some basic privileges.
These are specified in the [[user zettel|00001010040200]] with the key ''user-role''.
The following values are supported:

; [!reader|""reader""]

Changes to docs/manual/00001010070400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001010070400
title: Authorization and read-only mode
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk

modified: 20211103164251

It is possible to enable both the read-only mode of the Zettelstore __and__ authentication/authorization.
Both modes are independent from each other.
This gives four use cases:

; Not read-only, no authorization





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001010070400
title: Authorization and read-only mode
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211103164251

It is possible to enable both the read-only mode of the Zettelstore __and__ authentication/authorization.
Both modes are independent from each other.
This gives four use cases:

; Not read-only, no authorization

Changes to docs/manual/00001010070600.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001010070600
title: Access rules
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk

modified: 20211124142456

Whether an operation of the Zettelstore is allowed or rejected, depends on various factors.

The following rules are checked first, in this order:

# In read-only mode, every operation except the ""Read"" operation is rejected.
# If there is no owner, authentication is disabled and every operation is allowed for everybody.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001010070600
title: Access rules
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240711183714

Whether an operation of the Zettelstore is allowed or rejected, depends on various factors.

The following rules are checked first, in this order:

# In read-only mode, every operation except the ""Read"" operation is rejected.
# If there is no owner, authentication is disabled and every operation is allowed for everybody.
38
39
40
41
42
43
44
45
46
47
48
49
50
51
** If the zettel is the [[user zettel|00001010040200]] of the authenticated user, proceed as follows:
*** If some sensitive meta values are changed (e.g. user identifier, zettel role, user role, but not hashed password), reject the access
*** Since the user just updates some uncritical values, grant the access
   In other words: a user is allowed to change its user zettel, even if s/he has no writer privilege and if only uncritical data is changed.
** If the ''user-role'' of the user is ""reader"", reject the access.
** If the user is not allowed to create a new zettel, reject the access.
** Otherwise grant the access.
* Rename a zettel
** Reject the access.
   Only the owner of the Zettelstore is currently allowed to give a new identifier for a zettel.
* Delete a zettel
** Reject the access.
   Only the owner of the Zettelstore is allowed to delete a zettel.
   This may change in the future.







<
<
<




39
40
41
42
43
44
45



46
47
48
49
** If the zettel is the [[user zettel|00001010040200]] of the authenticated user, proceed as follows:
*** If some sensitive meta values are changed (e.g. user identifier, zettel role, user role, but not hashed password), reject the access
*** Since the user just updates some uncritical values, grant the access
   In other words: a user is allowed to change its user zettel, even if s/he has no writer privilege and if only uncritical data is changed.
** If the ''user-role'' of the user is ""reader"", reject the access.
** If the user is not allowed to create a new zettel, reject the access.
** Otherwise grant the access.



* Delete a zettel
** Reject the access.
   Only the owner of the Zettelstore is allowed to delete a zettel.
   This may change in the future.

Changes to docs/manual/00001010090100.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk

modified: 20220217180826

Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220217180826

Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key.

Changes to docs/manual/00001012000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231128183617

The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore.
Most integration with other systems and services is done through the API.
The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore.

=== Background
The API is HTTP-based and uses plain text and [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240711183736

The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore.
Most integration with other systems and services is done through the API.
The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore.

=== Background
The API is HTTP-based and uses plain text and [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software.
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
=== Working with zettel
* [[Create a new zettel|00001012053200]]
* [[Retrieve metadata and content of an existing zettel|00001012053300]]
* [[Retrieve metadata of an existing zettel|00001012053400]]
* [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]]
* [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]]
* [[Update metadata and content of a zettel|00001012054200]]
* [[Rename a zettel|00001012054400]]
* [[Delete a zettel|00001012054600]]

=== Various helper methods
* [[Retrieve administrative data|00001012070500]]
* [[Execute some commands|00001012080100]]
** [[Check for authentication|00001012080200]]
** [[Refresh internal data|00001012080500]]







<







30
31
32
33
34
35
36

37
38
39
40
41
42
43
=== Working with zettel
* [[Create a new zettel|00001012053200]]
* [[Retrieve metadata and content of an existing zettel|00001012053300]]
* [[Retrieve metadata of an existing zettel|00001012053400]]
* [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]]
* [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]]
* [[Update metadata and content of a zettel|00001012054200]]

* [[Delete a zettel|00001012054600]]

=== Various helper methods
* [[Retrieve administrative data|00001012070500]]
* [[Execute some commands|00001012080100]]
** [[Check for authentication|00001012080200]]
** [[Refresh internal data|00001012080500]]

Changes to docs/manual/00001012050600.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001012050600
title: API: Provide an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk

modified: 20220218130020

The [[authentication process|00001012050200]] provides you with an [[access token|00001012921000]].
Most API calls need such an access token, so that they know the identity of the caller.

You send the access token in the ""Authorization"" request header field, as described in [[RFC 6750, section 2.1|https://tools.ietf.org/html/rfc6750#section-2.1]].
You need to use the ""Bearer"" authentication scheme to transmit the access token.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001012050600
title: API: Provide an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218130020

The [[authentication process|00001012050200]] provides you with an [[access token|00001012921000]].
Most API calls need such an access token, so that they know the identity of the caller.

You send the access token in the ""Authorization"" request header field, as described in [[RFC 6750, section 2.1|https://tools.ietf.org/html/rfc6750#section-2.1]].
You need to use the ""Bearer"" authentication scheme to transmit the access token.

Changes to docs/manual/00001012051200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id: 00001012051200
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170810

To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
Always use the endpoint ''/z'' to work with a list of zettel.

Without further specifications, a plain text document is returned, with one line per zettel.
Each line contains in the first 14 characters the [[zettel identifier|00001006050000]].
Separated by a space character, the title of the zettel follows:

```sh






|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id: 00001012051200
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241216104355

To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].
Always use the endpoint ''/z'' to work with a list of zettel.

Without further specifications, a plain text document is returned, with one line per zettel.
Each line contains in the first 14 characters the [[zettel identifier|00001006050000]].
Separated by a space character, the title of the zettel follows:

```sh

Changes to docs/manual/00001012051400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20240219161831
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions.

A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below).
An empty search expression will select all zettel.
An empty list of action, or no valid action, returns the list of all selected zettel metadata.

Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''.







|


|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20241216104329
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions.

A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below).
An empty search expression will select all zettel.
An empty list of action, or no valid action, returns the list of all selected zettel metadata.

Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''.

105
106
107
108
109
110
111


112
113
114
115
116
117
118
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.


; ''REDIRECT'' (aggregate)
: Performs a HTTP redirect to the first selected zettel, using HTTP status code 302.
  The zettel identifier is in the body.
; ''REINDEX'' (aggregate)
: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call.
  It is not really an aggregate, since it is used only for its side effect.
  It is allowed to specify another aggregate.







>
>







105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'' (aggregate)
: Performs a HTTP redirect to the first selected zettel, using HTTP status code 302.
  The zettel identifier is in the body.
; ''REINDEX'' (aggregate)
: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call.
  It is not really an aggregate, since it is used only for its side effect.
  It is allowed to specify another aggregate.

Changes to docs/manual/00001012053300.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20230807170259

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'
The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

```sh
...
````

Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''part=PART''.






|



|





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20241216104429

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'
The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].

```sh
...
````

Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''part=PART''.

Changes to docs/manual/00001012053400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230807170155

The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

To retrieve the plain metadata of a zettel, use the query parameter ''part=meta''

````sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta'
title: API: Retrieve metadata of an existing zettel
role: manual






|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20241216104120

The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].

To retrieve the plain metadata of a zettel, use the query parameter ''part=meta''

````sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta'
title: API: Retrieve metadata of an existing zettel
role: manual

Changes to docs/manual/00001012053500.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230807170112

The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''.
If successful, the output is a symbolic expression value:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) (TEXT "evaluated") (SPACE) (TEXT "metadata") (SPACE) (TEXT "and") (SPACE) (TEXT "content") (SPACE) (TEXT "of") (SPACE) (TEXT "a") (SPACE) (TEXT "specific") (SPACE) (TEXT "zettel") (SPACE) (TEXT "is") (SPACE) (LITERAL-INPUT () "/z/{ID}") (TEXT ",") (SPACE) (TEXT "where") (SPACE) (LITERAL-INPUT () "{ID}") ...
```

To select another encoding, you must provide the query parameter ''enc=ENCODING''.
Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]].
In addition, you may provide a query parameter ''part=PART'' to select the relevant [[part|00001012920800]] of a zettel.
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=html&part=zettel'






|



|



|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20241216104145

The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''.
If successful, the output is a symbolic expression value:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz'
(BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") (LITERAL-INPUT () "{ID}") (TEXT " is a placeholder for the ") ...
```

To select another encoding, you must provide the query parameter ''enc=ENCODING''.
Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]].
In addition, you may provide a query parameter ''part=PART'' to select the relevant [[part|00001012920800]] of a zettel.
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=html&part=zettel'

Changes to docs/manual/00001012053600.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170019

The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__.
By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated.

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.
The same default values applies to this endpoint.

=== HTTP Status codes
; ''200''






|






|



|







1
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: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241216104306

The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__.
By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated.

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly'
(BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.
The same default values applies to this endpoint.

=== HTTP Status codes
; ''200''

Deleted docs/manual/00001012054400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
id: 00001012054400
title: API: Rename a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20221219154659

Renaming a zettel is effectively just specifying a new identifier for the zettel.
Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful.
If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations.

As a consequence, you cannot rename a zettel when its identifier is used in a read-only box.
This applies to all [[predefined zettel|00001005090000]], for example.

The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''.
```
# curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/z/00001000000000
```

Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused [[zettel identifier|00001006050000]].
If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''.
All other characters, besides those 14 digits, are effectively ignored.
However, the value should form a valid URL that could be used later to [[read the content|00001012053300]] of the freshly renamed zettel.

=== HTTP Status codes
; ''204''
: Rename was successful, there is no body in the response.
; ''400''
: Request was not valid.
  For example, the HTTP header did not contain a valid ''Destination'' key, or the new identifier is already in use.
; ''403''
: You are not allowed to delete the given zettel.
  In most cases you have either not enough [[access rights|00001010070600]] or at least one box containing the given identifier operates in read-only mode.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

=== Rationale for the MOVE method
HTTP [[standardizes|https://www.rfc-editor.org/rfc/rfc7231.txt]] eight methods.
None of them is conceptually close to a rename operation.

Everyone is free to ""invent"" some new method to be used in HTTP.
To avoid a divergency, there is a [[methods registry|https://www.iana.org/assignments/http-methods/]] that tracks those extensions.
The [[HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)|https://www.rfc-editor.org/rfc/rfc4918.txt]] defines the method MOVE that is quite close to the desired rename operation.
In fact, some command line tools use a ""move"" method for renaming files.

Therefore, Zettelstore adopts somehow WebDAV's MOVE method and its use of the ''Destination'' HTTP header key.
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































Changes to docs/manual/00001012080200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id: 00001012080200
title: API: Check for authentication
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220103224858
modified: 20220908163156

API clients typically wants to know, whether [[authentication is enabled|00001010040100]] or not.
If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication.
Then they try to [[obtain an access token|00001012050200]].
If authentication is disabled, these steps are not needed.

To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''.

```sh






|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id: 00001012080200
title: API: Check for authentication
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220103224858
modified: 20241216104549

API clients typically want to know, whether [[authentication is enabled|00001010040100]] or not.
If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication.
Then they try to [[obtain an access token|00001012050200]].
If authentication is disabled, these steps are not needed.

To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''.

```sh

Changes to docs/manual/00001012080500.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001012080500
title: API: Refresh internal data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211230230441
modified: 20220923104836

Zettelstore maintains some internal data to allow faster operations.

One example is the [[content search|00001012051400]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term.
Instead, all word are stored internally, with a list of zettel where they occur.

Another example is the way to determine which zettel are stored in a [[ZIP file|00001004011200]].
Scanning a ZIP file is a lengthy operation, therefore Zettelstore maintains a directory of zettel for each ZIP file.

All these internal data may become stale.
This should not happen, but when it comes e.g. to file handling, every operating systems behaves differently in very subtle ways.







|




|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001012080500
title: API: Refresh internal data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211230230441
modified: 20241216104624

Zettelstore maintains some internal data to allow faster operations.

One example is the [[content search|00001012051400]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term.
Instead, all words are stored internally, with a list of zettel where they occur.

Another example is the way to determine which zettel are stored in a [[ZIP file|00001004011200]].
Scanning a ZIP file is a lengthy operation, therefore Zettelstore maintains a directory of zettel for each ZIP file.

All these internal data may become stale.
This should not happen, but when it comes e.g. to file handling, every operating systems behaves differently in very subtle ways.

Changes to docs/manual/00001012920000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
id: 00001012920000
title: Endpoints used by the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230731162343

All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where:
; ''PREFIX''
: is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]],
; ''LETTER''
: is a single letter that specifies the resource type,
; ''ZETTEL-ID''
: is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]].

The following letters are currently in use:

|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic
| ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate
|       | PUT: [[renew access token|00001012050400]] |
| ''x'' | GET: [[retrieve administrative data|00001012070500]] | | E**x**ecute
|       | POST: [[execute command|00001012080100]]
| ''z'' | GET: [[list zettel|00001012051200]]/[[query zettel|00001012051400]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel
|       | POST: [[create new zettel|00001012053200]] | PUT: [[update zettel|00001012054200]]
|       |  | DELETE: [[delete zettel|00001012054600]]
|       |  | MOVE: [[rename zettel|00001012054400]]

The full URL will contain either the ""http"" oder ""https"" scheme, a host name, and an optional port number.

The API examples will assume the ""http"" schema, the local host ""127.0.0.1"", the default port ""23123"", and the default empty ''PREFIX'' ""/"".
Therefore, all URLs in the API documentation will begin with ""http://127.0.0.1:23123/"".






|



















<





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

27
28
29
30
31
id: 00001012920000
title: Endpoints used by the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240711183819

All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where:
; ''PREFIX''
: is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]],
; ''LETTER''
: is a single letter that specifies the resource type,
; ''ZETTEL-ID''
: is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]].

The following letters are currently in use:

|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic
| ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate
|       | PUT: [[renew access token|00001012050400]] |
| ''x'' | GET: [[retrieve administrative data|00001012070500]] | | E**x**ecute
|       | POST: [[execute command|00001012080100]]
| ''z'' | GET: [[list zettel|00001012051200]]/[[query zettel|00001012051400]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel
|       | POST: [[create new zettel|00001012053200]] | PUT: [[update zettel|00001012054200]]
|       |  | DELETE: [[delete zettel|00001012054600]]


The full URL will contain either the ""http"" oder ""https"" scheme, a host name, and an optional port number.

The API examples will assume the ""http"" schema, the local host ""127.0.0.1"", the default port ""23123"", and the default empty ''PREFIX'' ""/"".
Therefore, all URLs in the API documentation will begin with ""http://127.0.0.1:23123/"".

Changes to docs/manual/00001012920510.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001012920510
title: HTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

modified: 20210726193034

A zettel representation in HTML.
This representation is different form the [[web user interface|00001014000000]] as it contains the zettel representation only and no additional data such as the menu bar.

It is intended to be used by external clients.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001012920510
title: HTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210726193034

A zettel representation in HTML.
This representation is different form the [[web user interface|00001014000000]] as it contains the zettel representation only and no additional data such as the menu bar.

It is intended to be used by external clients.

Changes to docs/manual/00001012920519.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001012920519
title: Text Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

modified: 20210726193119

A zettel representation contains just all textual data of a zettel.
Could be used for creating a search index.

Every line may contain zero, one, or more words, separated by space character.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001012920519
title: Text Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210726193119

A zettel representation contains just all textual data of a zettel.
Could be used for creating a search index.

Every line may contain zero, one, or more words, separated by space character.

Changes to docs/manual/00001012920522.zettel.

1
2
3
4
5

6
7
8
9
10
11
id: 00001012920522
title: Zmk Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

modified: 20211124140857

A zettel representation that tries to recreate a [[Zettelmarkup|00001007000000]] representation of the zettel.
Useful if you want to convert [[other markup languages|00001008000000]] to Zettelmarkup (e.g. [[Markdown|00001008010000]]).

If transferred via HTTP, the content type will be ''text/plain''.





>






1
2
3
4
5
6
7
8
9
10
11
12
id: 00001012920522
title: Zmk Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124140857

A zettel representation that tries to recreate a [[Zettelmarkup|00001007000000]] representation of the zettel.
Useful if you want to convert [[other markup languages|00001008000000]] to Zettelmarkup (e.g. [[Markdown|00001008010000]]).

If transferred via HTTP, the content type will be ''text/plain''.

Changes to docs/manual/00001012920800.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001012920800
title: Values to specify zettel parts
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

modified: 20220214175335

When working with [[zettel|00001006000000]], you could work with the whole zettel, with its metadata, or with its content:
; [!zettel|''zettel'']
: Specifies that you work with a zettel as a whole.
  Contains identifier, metadata, and content of a zettel.
; [!meta|''meta'']





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001012920800
title: Values to specify zettel parts
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220214175335

When working with [[zettel|00001006000000]], you could work with the whole zettel, with its metadata, or with its content:
; [!zettel|''zettel'']
: Specifies that you work with a zettel as a whole.
  Contains identifier, metadata, and content of a zettel.
; [!meta|''meta'']

Changes to docs/manual/00001012921200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
id: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220201173115
modified: 20230807164817

Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has.
''N'' is an integer number between 0 and 62.[^Not all values in this range are used.]

The value ""0"" signals that something went wrong internally while determining the access rights.

A value of ""1"" says, that the current user has no access right for the given zettel.
In most cases, this value will not occur, because only zettel are presented, which are at least readable by the current user.

Values ""2"" to ""62"" are binary encoded values, where each bit signals a special right.

|=Bit number:|Bit value:|Meaning
| 1 |  2 | User is allowed to create a new zettel
| 2 |  4 | User is allowed to read the zettel
| 3 |  8 | User is allowed to update the zettel
| 4 | 16 | User is allowed to rename the zettel
| 5 | 32 | User is allowed to delete the zettel

The algorithm to calculate the actual access rights from the value is relatively simple:
# Search for the biggest bit value that is less than the rights value.
  This is an access right for the current user.
# Subtract the bit value from the rights value.
  Remember the difference as the new rights value.






|















|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
id: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220201173115
modified: 20240711183931

Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has.
''N'' is an integer number between 0 and 62.[^Not all values in this range are used.]

The value ""0"" signals that something went wrong internally while determining the access rights.

A value of ""1"" says, that the current user has no access right for the given zettel.
In most cases, this value will not occur, because only zettel are presented, which are at least readable by the current user.

Values ""2"" to ""62"" are binary encoded values, where each bit signals a special right.

|=Bit number:|Bit value:|Meaning
| 1 |  2 | User is allowed to create a new zettel
| 2 |  4 | User is allowed to read the zettel
| 3 |  8 | User is allowed to update the zettel
| 4 | 16 | (not in use; was assigned to an operation)
| 5 | 32 | User is allowed to delete the zettel

The algorithm to calculate the actual access rights from the value is relatively simple:
# Search for the biggest bit value that is less than the rights value.
  This is an access right for the current user.
# Subtract the bit value from the rights value.
  Remember the difference as the new rights value.

Changes to docs/manual/00001012930500.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012930500
title: Syntax of Symbolic Expressions
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20230403151127
modified: 20230703174218

=== Syntax of lists
A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.

Internally, lists are composed of __cells__.
A cell allows to store two values.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012930500
title: Syntax of Symbolic Expressions
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20230403151127
modified: 20240413160345

=== Syntax of lists
A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.

Internally, lists are composed of __cells__.
A cell allows to store two values.
67
68
69
70
71
72
73
74
75
76
77
To allow a string to contain a backslash, it also must be prefixed by one backslash.
Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character.
In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A).

=== See also
* Currently, Zettelstore uses [[sx|https://zettelstore.de/sx]] (""Symbolic eXPression Framework"") to implement symbolic expression.
  The project page might contain additional information about the full syntax.

  Zettelstore only uses lists, numbers, string, and symbols to represent zettel.







|



67
68
69
70
71
72
73
74
75
76
77
To allow a string to contain a backslash, it also must be prefixed by one backslash.
Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character.
In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A).

=== See also
* Currently, Zettelstore uses [[Sx|https://t73f.de/r/sx]] (""Symbolic eXpression framework"") to implement symbolic expressions.
  The project page might contain additional information about the full syntax.

  Zettelstore only uses lists, numbers, string, and symbols to represent zettel.

Changes to docs/manual/00001012931400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012931400
title: Encoding of Sz Block Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161803
modified: 20240123120132

=== ''PARA''
:::syntax
__Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A paragraph is just a list of inline elements.







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012931400
title: Encoding of Sz Block Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161803
modified: 20241216104739

=== ''PARA''
:::syntax
__Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A paragraph is just a list of inline elements.

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
:::
A list element is either a block or an inline.
If it is a block, it may contain a nested list.
=== ''DESCRIPTION''
:::syntax
__Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ &hellip; '')''.
:::
A description is a sequence of one ore more terms and values.

:::syntax
__DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A description term is just an inline-structured value.

:::syntax







|







43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
:::
A list element is either a block or an inline.
If it is a block, it may contain a nested list.
=== ''DESCRIPTION''
:::syntax
__Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ &hellip; '')''.
:::
A description is a sequence of one or more terms and values.

:::syntax
__DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A description term is just an inline-structured value.

:::syntax
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
Attributes may further specify the quotation.
The inline typically describes author / source of the quotation.

:::syntax
__VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] &hellip; '')'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A block region just treats the block to contain a verse.
Soft line break are transformed into hard line breaks to save the structure of the verse / poem.
Attributes may further specify something.
The inline typically describes author / source of the verse.

=== ''VERBATIM-*''
The following lists specifies some literal text of more than one line.
The structure is always the same, the initial symbol denotes the actual usage.
The content is encoded as a string, most likely to contain control characters that signals the end of a line.







|







122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
Attributes may further specify the quotation.
The inline typically describes author / source of the quotation.

:::syntax
__VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] &hellip; '')'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A block region just treats the block to contain a verse.
Soft line breaks are transformed into hard line breaks to save the structure of the verse / poem.
Attributes may further specify something.
The inline typically describes author / source of the verse.

=== ''VERBATIM-*''
The following lists specifies some literal text of more than one line.
The structure is always the same, the initial symbol denotes the actual usage.
The content is encoded as a string, most likely to contain control characters that signals the end of a line.

Changes to docs/manual/00001012931600.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20240122122448

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, typically a word.

=== ''SPACE''
:::syntax
__Space__ **=** ''(SPACE'' **[** String **]** '')''.
:::
Specifies some space, typically white space.
If the string is not given it is assumed to be ''" "'' (one space character).
Otherwise it contains the space characters.

=== ''SOFT''
:::syntax
__Soft__ **=** ''(SOFT)''.
:::
Denotes a soft line break.
It is typically translated into a space character, but signals the point in the textual content, where a line break occurred.

=== ''HARD''
:::syntax
__Hard__ **=** ''(HARD)''.
:::
Specifies a hard line break, i.e. the user wants to have a line break here.







|





|
<
<
<
<
<
<
<
<






|







1
2
3
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: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20241216104906

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, including white space characters.









=== ''SOFT''
:::syntax
__Soft__ **=** ''(SOFT)''.
:::
Denotes a soft line break.
It will be often encoded as a space character, but signals the point in the textual content, where a line break occurred.

=== ''HARD''
:::syntax
__Hard__ **=** ''(HARD)''.
:::
Specifies a hard line break, i.e. the user wants to have a line break here.

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
=== ''EMBED-BLOB''
:::syntax
__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''.
:::
If used if some processed image has to be embedded inside some inline material.
The first string specifies the syntax of the image content.
The second string contains the image content.
If the syntax is ""SVG"", the image content is not further encoded.
Otherwise a base64 encoding is used.

=== ''CITE''
:::syntax
__CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the citation key.







|







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
=== ''EMBED-BLOB''
:::syntax
__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''.
:::
If used if some processed image has to be embedded inside some inline material.
The first string specifies the syntax of the image content.
The second string contains the image content.
If the syntax is ""SVG"", the image content is not encoded further.
Otherwise a base64 encoding is used.

=== ''CITE''
:::syntax
__CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the citation key.
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
__InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as inserted.

:::syntax
__MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as highlighted for the reader (but was not important fto the original author).

:::syntax
__QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as quoted text.

:::syntax







|







138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
__InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as inserted.

:::syntax
__MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as highlighted for the reader (but was not important to the original author).

:::syntax
__QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as quoted text.

:::syntax

Changes to docs/manual/00001014000000.zettel.

1
2
3
4
5

6
7
8
id: 00001014000000
title: Web user interface
tags: #manual #webui #zettelstore
syntax: zmk
role: manual


The Web user interface is just a secondary way to interact with a Zettelstore.
Using external software that interacts via the [[API|00001012000000]] is the recommended way.





>



1
2
3
4
5
6
7
8
9
id: 00001014000000
title: Web user interface
tags: #manual #webui #zettelstore
syntax: zmk
role: manual
created: 20210126175322

The Web user interface is just a secondary way to interact with a Zettelstore.
Using external software that interacts via the [[API|00001012000000]] is the recommended way.

Changes to docs/manual/00001017000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001017000000
title: Tips and Tricks
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20220803170112
modified: 20231012154803

=== Welcome Zettel
* **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users.
  In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel.
  Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]].
* **Solution 1:**
*# Create a new zettel with all your references to internal, non-public zettel.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001017000000
title: Tips and Tricks
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20220803170112
modified: 20241216105111

=== Welcome Zettel
* **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users.
  In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel.
  Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]].
* **Solution 1:**
*# Create a new zettel with all your references to internal, non-public zettel.
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
* **Solution 2:** If you typically start your Zettelstore on the command line, you could use the ''-d DIR'' option for the [[''run''|00001004051000#d]] sub-command.
  In this case you are allowed to use the character ""''~''"".

  ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel''

  (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.)
* **Discussion:** Zettel files are synchronized between your computers via iCloud.
  Is does not matter, if one of your computer is offline / switched off.
  iCloud will synchronize the zettel files if it later comes online.

  However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time.
  It might take several seconds, maybe longer, that new new version of a zettel appears on the other computer.
  If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner.
  Zettelstore is intentionally not aware of any synchronization within its zettel boxes.

  If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information.







|



|




65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
* **Solution 2:** If you typically start your Zettelstore on the command line, you could use the ''-d DIR'' option for the [[''run''|00001004051000#d]] sub-command.
  In this case you are allowed to use the character ""''~''"".

  ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel''

  (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.)
* **Discussion:** Zettel files are synchronized between your computers via iCloud.
  Is does not matter, if one of your computers is offline or switched off.
  iCloud will synchronize the zettel files if it later comes online.

  However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time.
  It might take several seconds, maybe longer, that the new version of a zettel appears on the other computer.
  If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner.
  Zettelstore is intentionally not aware of any synchronization within its zettel boxes.

  If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information.

Changes to docs/manual/00001018000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001018000000
title: Troubleshooting
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211027105921
modified: 20240221134749

This page lists some problems and their solutions that may occur when using your Zettelstore.

=== Installation
* **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer.
  Therefore, it will not start Zettelstore.
** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click.
   A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore.
   This dialog is only resented once for a given Zettelstore executable.
* **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer.
** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allows to start Zettelstore.

=== Authentication
* **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner.
  But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message.
** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123''.
   The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema.
   To be secure by default, the Zettelstore will not work in an insecure environment.






|










|







1
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: 00001018000000
title: Troubleshooting
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211027105921
modified: 20241212153148

This page lists some problems and their solutions that may occur when using your Zettelstore.

=== Installation
* **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer.
  Therefore, it will not start Zettelstore.
** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click.
   A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore.
   This dialog is only resented once for a given Zettelstore executable.
* **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer.
** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allow to start Zettelstore.

=== Authentication
* **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner.
  But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message.
** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123''.
   The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema.
   To be secure by default, the Zettelstore will not work in an insecure environment.
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56






** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible.
** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]].

=== HTML content is not shown
* **Problem:** You have entered some HTML code as content for your Zettelstore, but this content is not shown on the Web User Interface.

  You may have entered a Zettel with syntax [[""html""|00001008000000#html]], or you have used an [[inline-zettel block|00001007031200]] with syntax ""html"", or you entered a Zettel with syntax [[""markdown""|00001008000000#markdown]] (or ""md"") and used some HTML code fragments.
** **Explanation:** Working with HTML code from unknown sources may lead so severe security problems.
   The HTML code may force web browsers to load more content from external server, it may contain malicious JavaScript code, it may reference to CSS artifacts that itself load from external servers and may contains malicious software.
   Zettelstore tries to do its best to ignore problematic HTML code, but it may fail.
   Either because of unknown bugs or because of yet unknown changes in the future.

   Zettelstore sets a restrictive [[Content Security Policy|https://www.w3.org/TR/CSP/]], but this depends on web browsers to implement them correctly and on users to not disable it.
   Zettelstore will not display any HTML code, which contains a ``<script>>`` or an ``<iframe>`` tag.
   But attackers may find other ways to deploy their malicious code.

   Therefore, Zettelstore disallows any HTML content as a default.
   If you know what you are doing, e.g. because you will never copy HTML code you do not understand, you can relax this default.
** **Solution 1:** If you want zettel with syntax ""html"" not to be ignored, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""html"".
** **Solution 2:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""markdown"".
** **Solution 3:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, **and** want to use HTML code within Zettelmarkup, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""zettelmarkup"".













|
|












>
>
>
>
>
>
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible.
** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]].

=== HTML content is not shown
* **Problem:** You have entered some HTML code as content for your Zettelstore, but this content is not shown on the Web User Interface.

  You may have entered a Zettel with syntax [[""html""|00001008000000#html]], or you have used an [[inline-zettel block|00001007031200]] with syntax ""html"", or you entered a Zettel with syntax [[""markdown""|00001008000000#markdown]] (or ""md"") and used some HTML code fragments.
** **Explanation:** Working with HTML code from unknown sources may lead to severe security problems.
   The HTML code may force web browsers to load more content from external server, it may contain malicious JavaScript code, it may reference to CSS artifacts that itself load from external servers and may contain malicious software.
   Zettelstore tries to do its best to ignore problematic HTML code, but it may fail.
   Either because of unknown bugs or because of yet unknown changes in the future.

   Zettelstore sets a restrictive [[Content Security Policy|https://www.w3.org/TR/CSP/]], but this depends on web browsers to implement them correctly and on users to not disable it.
   Zettelstore will not display any HTML code, which contains a ``<script>>`` or an ``<iframe>`` tag.
   But attackers may find other ways to deploy their malicious code.

   Therefore, Zettelstore disallows any HTML content as a default.
   If you know what you are doing, e.g. because you will never copy HTML code you do not understand, you can relax this default.
** **Solution 1:** If you want zettel with syntax ""html"" not to be ignored, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""html"".
** **Solution 2:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""markdown"".
** **Solution 3:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, **and** want to use HTML code within Zettelmarkup, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""zettelmarkup"".

=== Search for specific content
* **Problem:** If you are searching for zettel with zettel content ""EUPL"", the zettel with Zettelstore's [[License|00000000000004]] is not shown, but it does contain the character sequence ""EUPL"".
** **Solution:** The content of zettel with a zettel identifier less or equal ''00009999999999'' is not searched.
   These zettel are predefined zettel, sometimes computed zettel, with some content not related to your research.
   For these zettel, only the metadata can be searched.

Deleted docs/manual/20231128184200.zettel.

1
2
3
4
5
6
7
id: 20231128184200
title: manual
role: role
syntax: zmk
created: 20231128184200

Zettel with the role ""manual"" contain the manual of the zettelstore.
<
<
<
<
<
<
<














Changes to encoder/encoder.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package encoder

import (
	"errors"
	"fmt"
	"io"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/zettel/meta"
)

// Encoder is an interface that allows to encode different parts of a zettel.
type Encoder interface {
	WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error)







|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package encoder

import (
	"errors"
	"fmt"
	"io"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/zettel/meta"
)

// Encoder is an interface that allows to encode different parts of a zettel.
type Encoder interface {
	WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error)

Changes to encoder/encoder_blob_test.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//-----------------------------------------------------------------------------

package encoder_test

import (
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser.
)







|
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//-----------------------------------------------------------------------------

package encoder_test

import (
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser.
)

Changes to encoder/encoder_block_test.go.

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
	},
	{
		descr: "Simple text: Hello, world",
		zmk:   "Hello, world",
		expect: expectMap{
			encoderHTML:  "<p>Hello, world</p>",
			encoderMD:    "Hello, world",
			encoderSz:    `(BLOCK (PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`,
			encoderSHTML: `((p "Hello," " " "world"))`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple block comment",
		zmk:   "%%%\nNo\nrender\n%%%",







|
|







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
	},
	{
		descr: "Simple text: Hello, world",
		zmk:   "Hello, world",
		expect: expectMap{
			encoderHTML:  "<p>Hello, world</p>",
			encoderMD:    "Hello, world",
			encoderSz:    `(BLOCK (PARA (TEXT "Hello, world")))`,
			encoderSHTML: `((p "Hello, world"))`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple block comment",
		zmk:   "%%%\nNo\nrender\n%%%",
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
	},
	{
		descr: "Simple Heading",
		zmk:   `=== Top Job`,
		expect: expectMap{
			encoderHTML:  "<h2 id=\"top-job\">Top Job</h2>",
			encoderMD:    "# Top Job",
			encoderSz:    `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top") (SPACE) (TEXT "Job")))`,
			encoderSHTML: `((h2 (@ (id . "top-job")) "Top" " " "Job"))`,
			encoderText:  `Top Job`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List",
		zmk:   "* A\n* B\n* C",







|
|







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
	},
	{
		descr: "Simple Heading",
		zmk:   `=== Top Job`,
		expect: expectMap{
			encoderHTML:  "<h2 id=\"top-job\">Top Job</h2>",
			encoderMD:    "# Top Job",
			encoderSz:    `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top Job")))`,
			encoderSHTML: `((h2 (@ (id . "top-job")) "Top Job"))`,
			encoderText:  `Top Job`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List",
		zmk:   "* A\n* B\n* C",
172
173
174
175
176
177
178
179
180
181
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
	},
	{
		descr: "Simple Quote Block",
		zmk:   "<<<\nToBeOrNotToBe\n<<< Romeo Julia",
		expect: expectMap{
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo Julia</cite></blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSz:    `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) (TEXT "Romeo") (SPACE) (TEXT "Julia")))`,
			encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo" " " "Julia")))`,
			encoderText:  "ToBeOrNotToBe\nRomeo Julia",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quote Block with multiple paragraphs",
		zmk:   "<<<\nToBeOr\n\nNotToBe\n<<< Romeo",
		expect: expectMap{
			encoderHTML:  "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>",
			encoderMD:    "> ToBeOr\n\n> NotToBe",
			encoderSz:    `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (TEXT "Romeo")))`,
			encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`,
			encoderText:  "ToBeOr\nNotToBe\nRomeo",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Verse block",
		zmk: `"""
A line
  another line
Back

Paragraph

    Spacy  Para
""" Author`,
		expect: expectMap{
			encoderHTML:  "<div><p>A\u00a0line<br>\u00a0\u00a0another\u00a0line<br>Back</p><p>Paragraph</p><p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p><cite>Author</cite></div>",
			encoderMD:    "",
			encoderSz:    "(BLOCK (REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) (TEXT \"Author\")))",
			encoderSHTML: "((div (p \"A\" \"\u00a0\" \"line\" (br) \"\u00a0\u00a0\" \"another\" \"\u00a0\" \"line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0\" \"Spacy\" \"\u00a0\u00a0\" \"Para\") (cite \"Author\")))",
			encoderText:  "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor",
			encoderZmk:   "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author",
		},
	},
	{
		descr: "Span Block",
		zmk: `:::
A simple
   span
and much more
:::`,
		expect: expectMap{
			encoderHTML:  "<div><p>A simple  span and much more</p></div>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more")))))`,
			encoderSHTML: `((div (p "A" " " "simple" " " " " "span" " " "and" " " "much" " " "more")))`,
			encoderText:  `A simple  span and much more`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code",
		zmk:   "```\nHello\nWorld\n```",







|
|









|




















|
|












|

|
|







172
173
174
175
176
177
178
179
180
181
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
	},
	{
		descr: "Simple Quote Block",
		zmk:   "<<<\nToBeOrNotToBe\n<<< Romeo Julia",
		expect: expectMap{
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo Julia</cite></blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSz:    `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) (TEXT "Romeo Julia")))`,
			encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo Julia")))`,
			encoderText:  "ToBeOrNotToBe\nRomeo Julia",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quote Block with multiple paragraphs",
		zmk:   "<<<\nToBeOr\n\nNotToBe\n<<< Romeo",
		expect: expectMap{
			encoderHTML:  "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>",
			encoderMD:    "> ToBeOr\n>\n> NotToBe",
			encoderSz:    `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (TEXT "Romeo")))`,
			encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`,
			encoderText:  "ToBeOr\nNotToBe\nRomeo",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Verse block",
		zmk: `"""
A line
  another line
Back

Paragraph

    Spacy  Para
""" Author`,
		expect: expectMap{
			encoderHTML:  "<div><p>A\u00a0line<br>\u00a0\u00a0another\u00a0line<br>Back</p><p>Paragraph</p><p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p><cite>Author</cite></div>",
			encoderMD:    "",
			encoderSz:    "(BLOCK (REGION-VERSE () ((PARA (TEXT \"A\u00a0line\") (HARD) (TEXT \"\u00a0\u00a0another\u00a0line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (TEXT \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\"))) (TEXT \"Author\")))",
			encoderSHTML: "((div (p \"A\u00a0line\" (br) \"\u00a0\u00a0another\u00a0line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\") (cite \"Author\")))",
			encoderText:  "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor",
			encoderZmk:   "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author",
		},
	},
	{
		descr: "Span Block",
		zmk: `:::
A simple
   span
and much more
:::`,
		expect: expectMap{
			encoderHTML:  "<div><p>A simple    span and much more</p></div>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (REGION-BLOCK () ((PARA (TEXT "A simple") (SOFT) (TEXT "   span") (SOFT) (TEXT "and much more")))))`,
			encoderSHTML: `((div (p "A simple" " " "   span" " " "and much more")))`,
			encoderText:  `A simple  span and much more`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code",
		zmk:   "```\nHello\nWorld\n```",
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
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p></dd><dd><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with paragraphs as item",
		zmk:   "; Zettel\n: Paper\n\n  Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with keys, but no descriptions",
		zmk:   "; K1\n: D11\n: D12\n; K2\n; K3\n: D31",







|
|










|
|







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
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p></dd><dd><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with paragraphs as item",
		zmk:   "; Zettel\n: Paper\n\n  Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with keys, but no descriptions",
		zmk:   "; K1\n: D11\n: D12\n; K2\n; K3\n: D31",
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
	{
		descr: "Table with alignment and comment",
		zmk: `|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3`,
		expect: expectMap{
			encoderHTML:  `<table><thead><tr><td class="right">h1</td><td>h2</td><td class="center">h3</td></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`,
			encoderSHTML: `((table (thead (tr (td (@ (class . "right")) "h1") (td "h2") (td (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`,
			encoderText:  "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: `|=h1>|=h2|=h3:
|<c1|c2|c3
|f1|f2|=f3`,
		},
	},
	{







|


|







329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
	{
		descr: "Table with alignment and comment",
		zmk: `|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3`,
		expect: expectMap{
			encoderHTML:  `<table><thead><tr><th class="right">h1</th><th>h2</th><th class="center">h3</th></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`,
			encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`,
			encoderText:  "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: `|=h1>|=h2|=h3:
|<c1|c2|c3
|f1|f2|=f3`,
		},
	},
	{

Changes to encoder/encoder_inline_test.go.

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
	},
	{
		descr: "Simple text: Hello, world (inline)",
		zmk:   `Hello, world`,
		expect: expectMap{
			encoderHTML:  "Hello, world",
			encoderMD:    "Hello, world",
			encoderSz:    `(INLINE (TEXT "Hello,") (SPACE) (TEXT "world"))`,
			encoderSHTML: `("Hello," " " "world")`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Soft Break",
		zmk:   "soft\nbreak",







|
|







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
	},
	{
		descr: "Simple text: Hello, world (inline)",
		zmk:   `Hello, world`,
		expect: expectMap{
			encoderHTML:  "Hello, world",
			encoderMD:    "Hello, world",
			encoderSz:    `(INLINE (TEXT "Hello, world"))`,
			encoderSHTML: `("Hello, world")`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Soft Break",
		zmk:   "soft\nbreak",
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderHTML:  "&ldquo;quotes&rdquo;",
			encoderMD:    "<q>quotes</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "quotes" (@H "&rdquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderHTML:  `<span lang="de">&bdquo;quotes&ldquo;</span>`,
			encoderMD:    "<q>quotes</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes")))`,
			encoderSHTML: `((span (@ (lang . "de")) (@H "&bdquo;") "quotes" (@H "&ldquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Empty quotes (default)",
		zmk:   `""""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;&rdquo;`,
			encoderMD:    "<q></q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE ()))`,
			encoderSHTML: `((@L (@H "&ldquo;" "&rdquo;")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Empty quotes (unknown)",
		zmk:   `""""{lang=unknown}`,
		expect: expectMap{
			encoderHTML:  `<span lang="unknown">&quot;&quot;</span>`,
			encoderMD:    "<q></q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE (("lang" . "unknown"))))`,
			encoderSHTML: `((span (@ (lang . "unknown")) (@H "&quot;" "&quot;")))`,
			encoderText:  ``,
			encoderZmk:   `""""{lang="unknown"}`,
		},
	},
	{
		descr: "Nested quotes (default)",
		zmk:   `""say: ::""yes, ::""or?""::""::""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;say: <span>&lsquo;yes, <span>&ldquo;or?&rdquo;</span>&rsquo;</span>&rdquo;`,
			encoderMD:    "<q>say: <q>yes, <q>or?</q></q></q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "say:") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes,") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?")))))))`,
			encoderSHTML: `((@L (@H "&ldquo;") "say:" " " (span (@L (@H "&lsquo;") "yes," " " (span (@L (@H "&ldquo;") "or?" (@H "&rdquo;"))) (@H "&rsquo;"))) (@H "&rdquo;")))`,
			encoderText:  `say: yes, or?`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Two quotes",
		zmk:   `""yes"" or ""no""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;yes&rdquo; or &ldquo;no&rdquo;`,
			encoderMD:    "<q>yes</q> or <q>no</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (SPACE) (TEXT "or") (SPACE) (FORMAT-QUOTE () (TEXT "no")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "yes" (@H "&rdquo;")) " " "or" " " (@L (@H "&ldquo;") "no" (@H "&rdquo;")))`,
			encoderText:  `yes or no`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark formatting",
		zmk:   `##marked##`,







|











|











|











|











|
|
|









|
|
|







147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderHTML:  "&ldquo;quotes&rdquo;",
			encoderMD:    "&ldquo;quotes&rdquo;",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "quotes" (@H "&rdquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderHTML:  `<span lang="de">&bdquo;quotes&ldquo;</span>`,
			encoderMD:    "&bdquo;quotes&ldquo;",
			encoderSz:    `(INLINE (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes")))`,
			encoderSHTML: `((span (@ (lang . "de")) (@H "&bdquo;") "quotes" (@H "&ldquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Empty quotes (default)",
		zmk:   `""""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;&rdquo;`,
			encoderMD:    "&ldquo;&rdquo;",
			encoderSz:    `(INLINE (FORMAT-QUOTE ()))`,
			encoderSHTML: `((@L (@H "&ldquo;" "&rdquo;")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Empty quotes (unknown)",
		zmk:   `""""{lang=unknown}`,
		expect: expectMap{
			encoderHTML:  `<span lang="unknown">&quot;&quot;</span>`,
			encoderMD:    "&quot;&quot;",
			encoderSz:    `(INLINE (FORMAT-QUOTE (("lang" . "unknown"))))`,
			encoderSHTML: `((span (@ (lang . "unknown")) (@H "&quot;" "&quot;")))`,
			encoderText:  ``,
			encoderZmk:   `""""{lang="unknown"}`,
		},
	},
	{
		descr: "Nested quotes (default)",
		zmk:   `""say: ::""yes, ::""or?""::""::""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;say: <span>&lsquo;yes, <span>&ldquo;or?&rdquo;</span>&rsquo;</span>&rdquo;`,
			encoderMD:    `&ldquo;say: &lsquo;yes, &ldquo;or?&rdquo;&rsquo;&rdquo;`,
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "say: ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes, ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?")))))))`,
			encoderSHTML: `((@L (@H "&ldquo;") "say: " (span (@L (@H "&lsquo;") "yes, " (span (@L (@H "&ldquo;") "or?" (@H "&rdquo;"))) (@H "&rsquo;"))) (@H "&rdquo;")))`,
			encoderText:  `say: yes, or?`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Two quotes",
		zmk:   `""yes"" or ""no""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;yes&rdquo; or &ldquo;no&rdquo;`,
			encoderMD:    `&ldquo;yes&rdquo; or &ldquo;no&rdquo;`,
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (TEXT " or ") (FORMAT-QUOTE () (TEXT "no")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "yes" (@H "&rdquo;")) " or " (@L (@H "&ldquo;") "no" (@H "&rdquo;")))`,
			encoderText:  `yes or no`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark formatting",
		zmk:   `##marked##`,
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
	},
	{
		descr: "HTML in Code formatting",
		zmk:   "``<script `` abc",
		expect: expectMap{
			encoderHTML:  "<code>&lt;script </code> abc",
			encoderMD:    "`<script ` abc",
			encoderSz:    `(INLINE (LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`,
			encoderSHTML: `((code "<script ") " " "abc")`,
			encoderText:  `<script  abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Input formatting",
		zmk:   `''input''`,
		expect: expectMap{
			encoderHTML:  `<kbd>input</kbd>`,
			encoderMD:    "input",
			encoderSz:    `(INLINE (LITERAL-INPUT () "input"))`,
			encoderSHTML: `((kbd "input"))`,
			encoderText:  `input`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Output formatting",
		zmk:   `==output==`,
		expect: expectMap{
			encoderHTML:  `<samp>output</samp>`,
			encoderMD:    "output",
			encoderSz:    `(INLINE (LITERAL-OUTPUT () "output"))`,
			encoderSHTML: `((samp "output"))`,
			encoderText:  `output`,
			encoderZmk:   useZmk,
		},
	},
	{







|
|









|











|







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
	},
	{
		descr: "HTML in Code formatting",
		zmk:   "``<script `` abc",
		expect: expectMap{
			encoderHTML:  "<code>&lt;script </code> abc",
			encoderMD:    "`<script ` abc",
			encoderSz:    `(INLINE (LITERAL-CODE () "<script ") (TEXT " abc"))`,
			encoderSHTML: `((code "<script ") " abc")`,
			encoderText:  `<script  abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Input formatting",
		zmk:   `''input''`,
		expect: expectMap{
			encoderHTML:  `<kbd>input</kbd>`,
			encoderMD:    "`input`",
			encoderSz:    `(INLINE (LITERAL-INPUT () "input"))`,
			encoderSHTML: `((kbd "input"))`,
			encoderText:  `input`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Output formatting",
		zmk:   `==output==`,
		expect: expectMap{
			encoderHTML:  `<samp>output</samp>`,
			encoderMD:    "`output`",
			encoderSz:    `(INLINE (LITERAL-OUTPUT () "output"))`,
			encoderSHTML: `((samp "output"))`,
			encoderText:  `output`,
			encoderZmk:   useZmk,
		},
	},
	{
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderHTML:  `<span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span>`,
			encoderMD:    "<q>abc</q>",
			encoderSz:    `(INLINE (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSHTML: `((span (@ (lang . "fr")) (@L (@H "&laquo;" "&nbsp;") "abc" (@H "&nbsp;" "&raquo;"))))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
		},
	},
	{







|







315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderHTML:  `<span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span>`,
			encoderMD:    "&laquo;&nbsp;abc&nbsp;&raquo;",
			encoderSz:    `(INLINE (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSHTML: `((span (@ (lang . "fr")) (@L (@H "&laquo;" "&nbsp;") "abc" (@H "&nbsp;" "&raquo;"))))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
		},
	},
	{
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
		},
	}, {
		descr: "No comment",
		zmk:   `% comment`,
		expect: expectMap{
			encoderHTML:  `% comment`,
			encoderMD:    "% comment",
			encoderSz:    `(INLINE (TEXT "%") (SPACE) (TEXT "comment"))`,
			encoderSHTML: `("%" " " "comment")`,
			encoderText:  `% comment`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment (nogen HTML)",
		zmk:   `%% line comment`,







|
|







351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
		},
	}, {
		descr: "No comment",
		zmk:   `% comment`,
		expect: expectMap{
			encoderHTML:  `% comment`,
			encoderMD:    "% comment",
			encoderSz:    `(INLINE (TEXT "% comment"))`,
			encoderSHTML: `("% comment")`,
			encoderText:  `% comment`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment (nogen HTML)",
		zmk:   `%% line comment`,
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
	},
	{
		descr: "Mark with text",
		zmk:   `[!mark|with text]`,
		expect: expectMap{
			encoderHTML:  `<a id="mark">with text</a>`,
			encoderMD:    "with text",
			encoderSz:    `(INLINE (MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`,
			encoderSHTML: `((a (@ (id . "mark")) "with" " " "text"))`,
			encoderText:  `with text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Link",
		zmk:   `[[link|00000000000000]]`,







|
|







435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
	},
	{
		descr: "Mark with text",
		zmk:   `[!mark|with text]`,
		expect: expectMap{
			encoderHTML:  `<a id="mark">with text</a>`,
			encoderMD:    "with text",
			encoderSz:    `(INLINE (MARK "mark" "mark" "mark" (TEXT "with text")))`,
			encoderSHTML: `((a (@ (id . "mark")) "with text"))`,
			encoderText:  `with text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Link",
		zmk:   `[[link|00000000000000]]`,
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
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Link",
		zmk:   `[[abc]]`,
		expect: expectMap{
			encoderHTML:  `<a class="external" href="abc">abc</a>`,
			encoderMD:    "[abc](abc)",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "abc"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "abc")) "abc"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple URL",
		zmk:   `[[https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<a class="external" href="https://zettelstore.de">https://zettelstore.de</a>`,
			encoderMD:    "<https://zettelstore.de>",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "https://zettelstore.de"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "URL with Text",
		zmk:   `[[Home|https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<a class="external" href="https://zettelstore.de">Home</a>`,
			encoderMD:    "[Home](https://zettelstore.de)",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "Home"))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,







|


|








|


|








|


|







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
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Link",
		zmk:   `[[abc]]`,
		expect: expectMap{
			encoderHTML:  `<a href="abc" rel="external">abc</a>`,
			encoderMD:    "[abc](abc)",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "abc"))`,
			encoderSHTML: `((a (@ (href . "abc") (rel . "external")) "abc"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple URL",
		zmk:   `[[https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<a href="https://zettelstore.de" rel="external">https://zettelstore.de</a>`,
			encoderMD:    "<https://zettelstore.de>",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSHTML: `((a (@ (href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "URL with Text",
		zmk:   `[[Home|https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<a href="https://zettelstore.de" rel="external">Home</a>`,
			encoderMD:    "[Home](https://zettelstore.de)",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSHTML: `((a (@ (href . "https://zettelstore.de") (rel . "external")) "Home"))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,

Changes to encoder/encoder_test.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package encoder_test

import (
	"fmt"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/encoder/htmlenc"  // Allow to use HTML encoder.







|
|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package encoder_test

import (
	"fmt"
	"strings"
	"testing"

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/encoder/htmlenc"  // Allow to use HTML encoder.

Changes to encoder/htmlenc/htmlenc.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33


34

35
36
37
38
39
40
41
42
43
44
45
46
47
48

49
50
51
52
53
54
55
// Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client.
package htmlenc

import (
	"io"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {


	encoder.Register(api.EncoderHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) })

}

// Create an encoder.
func Create(params *encoder.CreateParameter) *Encoder {
	// We need a new transformer every time, because tx.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{
		tx:      szenc.NewTransformer(),
		th:      shtml.NewEvaluator(1),
		lang:    params.Lang,
		textEnc: textenc.Create(),
	}
}


type Encoder struct {
	tx      *szenc.Transformer
	th      *shtml.Evaluator
	lang    string
	textEnc *textenc.Encoder
}








|
|
|
|









>
>
|
>














>







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client.
package htmlenc

import (
	"io"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(
		api.EncoderHTML,
		func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) },
	)
}

// Create an encoder.
func Create(params *encoder.CreateParameter) *Encoder {
	// We need a new transformer every time, because tx.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{
		tx:      szenc.NewTransformer(),
		th:      shtml.NewEvaluator(1),
		lang:    params.Lang,
		textEnc: textenc.Create(),
	}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	tx      *szenc.Transformer
	th      *shtml.Evaluator
	lang    string
	textEnc *textenc.Encoder
}

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
	}

	xast := he.tx.GetSz(&zn.Ast)
	hast, err := he.th.Evaluate(xast, &env)
	if err != nil {
		return 0, err
	}
	hen := he.th.Endnotes(&env)

	var head sx.ListBuilder
	head.Add(shtml.SymHead)
	head.Add(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.String("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta))
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
		he.textEnc.WriteInlines(&sb, &isTitle)
	} else {
		sb.Write(zn.Meta.Zid.Bytes())
	}
	head.Add(sx.MakeList(shtml.SymAttrTitle, sx.String(sb.String())))

	var body sx.ListBuilder
	body.Add(shtml.SymBody)
	if hasTitle {
		body.Add(htitle.Cons(shtml.SymH1))
	}
	body.ExtendBang(hast)
	if hen != nil {
		body.Add(sx.Cons(shtml.SymHR, nil))
		body.Add(hen)
	}

	doc := sx.MakeList(
		sxhtml.SymDoctype,
		sx.MakeList(shtml.SymHtml, head.List(), body.List()),
	)

	gen := sxhtml.NewGenerator(sxhtml.WithNewline)
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator(sxhtml.WithNewline)
	return gen.WriteListHTML(w, hm)
}


func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks encodes a block slice.
func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env)
	if err == nil {
		gen := sxhtml.NewGenerator()
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, he.th.Endnotes(&env))
		length += l
		return length, err2
	}
	return 0, err
}

// WriteInlines writes an inline slice to the writer







|



|







|














|


|










|



>















|







78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
	}

	xast := he.tx.GetSz(&zn.Ast)
	hast, err := he.th.Evaluate(xast, &env)
	if err != nil {
		return 0, err
	}
	hen := shtml.Endnotes(&env)

	var head sx.ListBuilder
	head.Add(shtml.SymHead)
	head.Add(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta))
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
		he.textEnc.WriteInlines(&sb, &isTitle)
	} else {
		sb.Write(zn.Meta.Zid.Bytes())
	}
	head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String())))

	var body sx.ListBuilder
	body.Add(shtml.SymBody)
	if hasTitle {
		body.Add(htitle.Cons(shtml.SymH1))
	}
	body.ExtendBang(hast)
	if hen != nil {
		body.Add(sx.Cons(shtml.SymHR, nil))
		body.Add(hen)
	}

	doc := sx.MakeList(
		sxhtml.SymDoctype,
		sx.MakeList(shtml.SymHTML, head.List(), body.List()),
	)

	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteListHTML(w, hm)
}

// WriteContent encodes the zettel content.
func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks encodes a block slice.
func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env)
	if err == nil {
		gen := sxhtml.NewGenerator()
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, shtml.Endnotes(&env))
		length += l
		return length, err2
	}
	return 0, err
}

// WriteInlines writes an inline slice to the writer

Changes to encoder/mdenc/mdenc.go.

13
14
15
16
17
18
19
20


21
22
23
24
25
26


27

28
29
30
31

32


33

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97


98
99
100
101





















102
103
104
105
106
107
108

// Package mdenc encodes the abstract syntax tree back into Markdown.
package mdenc

import (
	"io"

	"zettelstore.de/client.fossil/api"


	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {


	encoder.Register(api.EncoderMD, func(*encoder.CreateParameter) encoder.Encoder { return Create() })

}

// Create an encoder.
func Create() *Encoder { return &myME }




type Encoder struct{}


var myME Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	v.acceptMeta(zn.InhMeta, evalMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteByte('\n')
	}
	ast.Walk(v, &zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
		key := p.Key
		v.b.WriteStrings(key, ": ")
		if meta.Type(key) == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			ast.Walk(v, &is)
		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}


func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
	ast.Walk(v, bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w)
	ast.Walk(v, is)
	length, err := v.b.Flush()
	return length, err
}

// visitor writes the abstract syntax tree to an EncWriter.
type visitor struct {
	b          encoder.EncWriter
	listInfo   []int
	listPrefix string


}

func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewEncWriter(w)}





















}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.VerbatimNode:







|
>
>






>
>
|
>



|
>
|
>
>
|
>
|
<


|
|












|
|



















>
|
|



|
|






|
|







|
|
|
>
>


|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

// Package mdenc encodes the abstract syntax tree back into Markdown.
package mdenc

import (
	"io"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(
		api.EncoderMD,
		func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) },
	)
}

// Create an encoder.
func Create(params *encoder.CreateParameter) *Encoder {
	return &Encoder{lang: params.Lang}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	lang string
}


// WriteZettel writes the encoded zettel to the writer.
func (me *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, me.lang)
	v.acceptMeta(zn.InhMeta, evalMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteByte('\n')
	}
	ast.Walk(v, &zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.
func (me *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, me.lang)
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
		key := p.Key
		v.b.WriteStrings(key, ": ")
		if meta.Type(key) == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			ast.Walk(v, &is)
		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}

// WriteContent encodes the zettel content.
func (me *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return me.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (me *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w, me.lang)
	ast.Walk(v, bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (me *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w, me.lang)
	ast.Walk(v, is)
	length, err := v.b.Flush()
	return length, err
}

// visitor writes the abstract syntax tree to an EncWriter.
type visitor struct {
	b            encoder.EncWriter
	listInfo     []int
	listPrefix   string
	langStack    shtml.LangStack
	quoteNesting uint
}

func newVisitor(w io.Writer, lang string) *visitor {
	return &visitor{b: encoder.NewEncWriter(w), langStack: shtml.NewLangStack(lang)}
}

// pushAttribute adds the current attributes to the visitor.
func (v *visitor) pushAttributes(a attrs.Attributes) {
	if value, ok := a.Get("lang"); ok {
		v.langStack.Push(value)
	} else {
		v.langStack.Dup()
	}
}

// popAttributes removes the current attributes from the visitor.
func (v *visitor) popAttributes() { v.langStack.Pop() }

// getLanguage returns the current language,
func (v *visitor) getLanguage() string { return v.langStack.Top() }

func (v *visitor) getQuotes() (string, string, bool) {
	qi := shtml.GetQuoteInfo(v.getLanguage())
	leftQ, rightQ := qi.GetQuotes(v.quoteNesting)
	return leftQ, rightQ, qi.GetNBSp()
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.VerbatimNode:
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		return nil // Should write no content
	case *ast.TableNode:
		return nil // Should write no content
	case *ast.TextNode:
		v.visitText(n)
	case *ast.SpaceNode:
		v.b.WriteString(n.Lexeme)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.FootnoteNode:







<
<
|







148
149
150
151
152
153
154


155
156
157
158
159
160
161
162
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		return nil // Should write no content
	case *ast.TableNode:
		return nil // Should write no content
	case *ast.TextNode:


		v.b.WriteString(n.Text)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.FootnoteNode:
179
180
181
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
	}
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	if rn.Kind != ast.RegionQuote {
		return
	}



	first := true
	for _, bn := range rn.Blocks {
		pn, ok := bn.(*ast.ParaNode)
		if !ok {
			continue
		}
		if !first {
			v.b.WriteString("\n\n")
		}
		first = false
		v.b.WriteString("> ")
		ast.Walk(v, &pn.Inlines)
	}
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {



	const headingSigns = "###### "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:])
	ast.Walk(v, &hn.Inlines)
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	switch ln.Kind {







>
>
>







|








>
>
>







209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
	}
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	if rn.Kind != ast.RegionQuote {
		return
	}
	v.pushAttributes(rn.Attrs)
	defer v.popAttributes()

	first := true
	for _, bn := range rn.Blocks {
		pn, ok := bn.(*ast.ParaNode)
		if !ok {
			continue
		}
		if !first {
			v.b.WriteString("\n>\n")
		}
		first = false
		v.b.WriteString("> ")
		ast.Walk(v, &pn.Inlines)
	}
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	v.pushAttributes(hn.Attrs)
	defer v.popAttributes()

	const headingSigns = "###### "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:])
	ast.Walk(v, &hn.Inlines)
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	switch ln.Kind {
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
			ast.Walk(v, in)
		}
	}

	v.listPrefix = prefix
}

func (v *visitor) visitText(tn *ast.TextNode) {
	v.b.WriteString(tn.Text)
}

func (v *visitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteByte('\n')
	}
	if l := len(v.listInfo); l > 0 {
		if v.listPrefix == "" {
			v.writeSpaces(4*l - 4 + v.listInfo[l-1])
		} else {
			v.writeSpaces(4*l - 4)
			v.b.WriteString(v.listPrefix)
		}
	}
}

func (v *visitor) visitLink(ln *ast.LinkNode) {



	v.writeReference(ln.Ref, ln.Inlines)
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {



	v.b.WriteByte('!')
	v.writeReference(en.Ref, en.Inlines)
}

func (v *visitor) writeReference(ref *ast.Reference, is ast.InlineSlice) {
	if ref.State == ast.RefStateQuery {
		ast.Walk(v, &is)







<
<
<
<

















>
>
>




>
>
>







298
299
300
301
302
303
304




305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
			ast.Walk(v, in)
		}
	}

	v.listPrefix = prefix
}





func (v *visitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteByte('\n')
	}
	if l := len(v.listInfo); l > 0 {
		if v.listPrefix == "" {
			v.writeSpaces(4*l - 4 + v.listInfo[l-1])
		} else {
			v.writeSpaces(4*l - 4)
			v.b.WriteString(v.listPrefix)
		}
	}
}

func (v *visitor) visitLink(ln *ast.LinkNode) {
	v.pushAttributes(ln.Attrs)
	defer v.popAttributes()

	v.writeReference(ln.Ref, ln.Inlines)
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.pushAttributes(en.Attrs)
	defer v.popAttributes()

	v.b.WriteByte('!')
	v.writeReference(en.Ref, en.Inlines)
}

func (v *visitor) writeReference(ref *ast.Reference, is ast.InlineSlice) {
	if ref.State == ast.RefStateQuery {
		ast.Walk(v, &is)
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
	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {



	switch fn.Kind {
	case ast.FormatEmph:
		v.b.WriteByte('*')
		ast.Walk(v, &fn.Inlines)
		v.b.WriteByte('*')
	case ast.FormatStrong:
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.b.WriteString("<q>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</q>")
	case ast.FormatMark:
		v.b.WriteString("<mark>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</mark>")
	default:
		ast.Walk(v, &fn.Inlines)
	}
}
















func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralProg:
		v.b.WriteByte('`')
		v.b.Write(ln.Content)
		v.b.WriteByte('`')
	case ast.LiteralComment, ast.LiteralHTML: // ignore everything
	default:
		v.b.Write(ln.Content)
	}







>
>
>










|
<
<








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



|







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
	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	v.pushAttributes(fn.Attrs)
	defer v.popAttributes()

	switch fn.Kind {
	case ast.FormatEmph:
		v.b.WriteByte('*')
		ast.Walk(v, &fn.Inlines)
		v.b.WriteByte('*')
	case ast.FormatStrong:
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.writeQuote(fn)


	case ast.FormatMark:
		v.b.WriteString("<mark>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</mark>")
	default:
		ast.Walk(v, &fn.Inlines)
	}
}

func (v *visitor) writeQuote(fn *ast.FormatNode) {
	leftQ, rightQ, withNbsp := v.getQuotes()
	v.b.WriteString(leftQ)
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.quoteNesting++
	ast.Walk(v, &fn.Inlines)
	v.quoteNesting--
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.b.WriteString(rightQ)
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralProg, ast.LiteralInput, ast.LiteralOutput:
		v.b.WriteByte('`')
		v.b.Write(ln.Content)
		v.b.WriteByte('`')
	case ast.LiteralComment, ast.LiteralHTML: // ignore everything
	default:
		v.b.Write(ln.Content)
	}

Changes to encoder/shtmlenc/shtmlenc.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

// Package shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML.
package shtmlenc

import (
	"io"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/zettel/meta"
)

func init() {







|
|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

// Package shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML.
package shtmlenc

import (
	"io"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/zettel/meta"
)

func init() {
37
38
39
40
41
42
43

44
45
46
47
48
49
50
	return &Encoder{
		tx:   szenc.NewTransformer(),
		th:   shtml.NewEvaluator(1),
		lang: params.Lang,
	}
}


type Encoder struct {
	tx   *szenc.Transformer
	th   *shtml.Evaluator
	lang string
}

// WriteZettel writes the encoded zettel to the writer.







>







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
	return &Encoder{
		tx:   szenc.NewTransformer(),
		th:   shtml.NewEvaluator(1),
		lang: params.Lang,
	}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	tx   *szenc.Transformer
	th   *shtml.Evaluator
	lang string
}

// WriteZettel writes the encoded zettel to the writer.
68
69
70
71
72
73
74

75
76
77
78
79
80
81
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, metaSHTML)
}


func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)







>







69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, metaSHTML)
}

// WriteContent encodes the zettel content.
func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)

Changes to encoder/szenc/szenc.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

// Package szenc encodes the abstract syntax tree into a s-expr for zettel.
package szenc

import (
	"io"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSz, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create a S-expr encoder
func Create() *Encoder {
	// We need a new transformer every time, because trans.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{trans: NewTransformer()}
}


type Encoder struct {
	trans *Transformer
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	content := enc.trans.GetSz(&zn.Ast)
	meta := enc.trans.GetMeta(zn.InhMeta, evalMeta)
	return sx.MakeList(meta, content).Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	return enc.trans.GetMeta(m, evalMeta).Print(w)
}


func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	return enc.trans.GetSz(bs).Print(w)
}

// WriteInlines writes an inline slice to the writer
func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	return enc.trans.GetSz(is).Print(w)
}







|
|
















>
















>













13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 szenc encodes the abstract syntax tree into a s-expr for zettel.
package szenc

import (
	"io"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSz, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create a S-expr encoder
func Create() *Encoder {
	// We need a new transformer every time, because trans.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{trans: NewTransformer()}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	trans *Transformer
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	content := enc.trans.GetSz(&zn.Ast)
	meta := enc.trans.GetMeta(zn.InhMeta, evalMeta)
	return sx.MakeList(meta, content).Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	return enc.trans.GetMeta(m, evalMeta).Print(w)
}

// WriteContent encodes the zettel content.
func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	return enc.trans.GetSz(bs).Print(w)
}

// WriteInlines writes an inline slice to the writer
func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	return enc.trans.GetSz(is).Print(w)
}

Changes to encoder/szenc/transform.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

35
36
37
38

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package szenc

import (
	"encoding/base64"
	"fmt"
	"strings"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/sz"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	t := Transformer{}
	return &t
}


type Transformer struct {
	inVerse bool
}


func (t *Transformer) GetSz(node ast.Node) *sx.Pair {
	switch n := node.(type) {
	case *ast.BlockSlice:
		return t.getBlockList(n).Cons(sz.SymBlock)
	case *ast.InlineSlice:
		return t.getInlineList(*n).Cons(sz.SymInline)
	case *ast.ParaNode:
		return t.getInlineList(n.Inlines).Cons(sz.SymPara)
	case *ast.VerbatimNode:
		return sx.MakeList(
			mapGetS(mapVerbatimKindS, n.Kind),
			getAttributes(n.Attrs),
			sx.String(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.String(n.Fragment)).
			Cons(sx.String(n.Slug)).
			Cons(getAttributes(n.Attrs)).
			Cons(sx.Int64(int64(n.Level))).
			Cons(sz.SymHeading)
	case *ast.HRuleNode:
		return sx.MakeList(sz.SymThematic, getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return sx.MakeList(sz.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sx.MakeList(sz.SymText, sx.String(n.Text))
	case *ast.SpaceNode:
		if t.inVerse {
			return sx.MakeList(sz.SymSpace, sx.String(n.Lexeme))
		}
		return sx.MakeList(sz.SymSpace)
	case *ast.BreakNode:
		if n.Hard {
			return sx.MakeList(sz.SymHard)
		}
		return sx.MakeList(sz.SymSoft)
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.String(n.Syntax)).
			Cons(getReference(n.Ref)).
			Cons(getAttributes(n.Attrs)).
			Cons(sz.SymEmbed)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.String(n.Key)).
			Cons(getAttributes(n.Attrs)).
			Cons(sz.SymCite)
	case *ast.FootnoteNode:
		// (ENDNODE attrs InlineElement ...)
		return t.getInlineList(n.Inlines).Cons(getAttributes(n.Attrs)).Cons(sz.SymEndnote)
	case *ast.MarkNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.String(n.Fragment)).
			Cons(sx.String(n.Slug)).
			Cons(sx.String(n.Mark)).
			Cons(sz.SymMark)
	case *ast.FormatNode:
		return t.getInlineList(n.Inlines).
			Cons(getAttributes(n.Attrs)).
			Cons(mapGetS(mapFormatKindS, n.Kind))
	case *ast.LiteralNode:
		return sx.MakeList(
			mapGetS(mapLiteralKindS, n.Kind),
			getAttributes(n.Attrs),
			sx.String(string(n.Content)),
		)
	}
	return sx.MakeList(sz.SymUnknown, sx.String(fmt.Sprintf("%T %v", node, node)))
}

var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
	ast.VerbatimZettel:  sz.SymVerbatimZettel,
	ast.VerbatimProg:    sz.SymVerbatimProg,
	ast.VerbatimEval:    sz.SymVerbatimEval,
	ast.VerbatimMath:    sz.SymVerbatimMath,







|
|
|











>




>












|





|
|
















|
<
<
<
<
<









|







|







|
|
|









|


|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77





78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package szenc

import (
	"encoding/base64"
	"fmt"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/sz"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	t := Transformer{}
	return &t
}

// Transformer contains all data needed to transform into a s-expression.
type Transformer struct {
	inVerse bool
}

// GetSz transforms the given node into a sx list.
func (t *Transformer) GetSz(node ast.Node) *sx.Pair {
	switch n := node.(type) {
	case *ast.BlockSlice:
		return t.getBlockList(n).Cons(sz.SymBlock)
	case *ast.InlineSlice:
		return t.getInlineList(*n).Cons(sz.SymInline)
	case *ast.ParaNode:
		return t.getInlineList(n.Inlines).Cons(sz.SymPara)
	case *ast.VerbatimNode:
		return sx.MakeList(
			mapGetS(mapVerbatimKindS, n.Kind),
			getAttributes(n.Attrs),
			sx.MakeString(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Fragment)).
			Cons(sx.MakeString(n.Slug)).
			Cons(getAttributes(n.Attrs)).
			Cons(sx.Int64(int64(n.Level))).
			Cons(sz.SymHeading)
	case *ast.HRuleNode:
		return sx.MakeList(sz.SymThematic, getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return sx.MakeList(sz.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sx.MakeList(sz.SymText, sx.MakeString(n.Text))





	case *ast.BreakNode:
		if n.Hard {
			return sx.MakeList(sz.SymHard)
		}
		return sx.MakeList(sz.SymSoft)
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Syntax)).
			Cons(getReference(n.Ref)).
			Cons(getAttributes(n.Attrs)).
			Cons(sz.SymEmbed)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Key)).
			Cons(getAttributes(n.Attrs)).
			Cons(sz.SymCite)
	case *ast.FootnoteNode:
		// (ENDNODE attrs InlineElement ...)
		return t.getInlineList(n.Inlines).Cons(getAttributes(n.Attrs)).Cons(sz.SymEndnote)
	case *ast.MarkNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Fragment)).
			Cons(sx.MakeString(n.Slug)).
			Cons(sx.MakeString(n.Mark)).
			Cons(sz.SymMark)
	case *ast.FormatNode:
		return t.getInlineList(n.Inlines).
			Cons(getAttributes(n.Attrs)).
			Cons(mapGetS(mapFormatKindS, n.Kind))
	case *ast.LiteralNode:
		return sx.MakeList(
			mapGetS(mapLiteralKindS, n.Kind),
			getAttributes(n.Attrs),
			sx.MakeString(string(n.Content)),
		)
	}
	return sx.MakeList(sz.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node)))
}

var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
	ast.VerbatimZettel:  sz.SymVerbatimZettel,
	ast.VerbatimProg:    sz.SymVerbatimProg,
	ast.VerbatimEval:    sz.SymVerbatimEval,
	ast.VerbatimMath:    sz.SymVerbatimMath,
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
func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
	return t.getInlineList(cell.Inlines).Cons(mapGetS(alignmentSymbolS, cell.Align))
}

func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
	var lastObj sx.Object
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sx.String(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sx.MakeList(
		sz.SymBLOB,
		t.getInlineList(bn.Description),
		sx.String(bn.Syntax),
		lastObj,
	)
}

var mapRefStateLink = map[ast.RefState]*sx.Symbol{
	ast.RefStateInvalid:  sz.SymLinkInvalid,
	ast.RefStateZettel:   sz.SymLinkZettel,
	ast.RefStateSelf:     sz.SymLinkSelf,
	ast.RefStateFound:    sz.SymLinkFound,
	ast.RefStateBroken:   sz.SymLinkBroken,
	ast.RefStateHosted:   sz.SymLinkHosted,
	ast.RefStateBased:    sz.SymLinkBased,
	ast.RefStateQuery:    sz.SymLinkQuery,
	ast.RefStateExternal: sz.SymLinkExternal,
}

func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair {
	return t.getInlineList(ln.Inlines).
		Cons(sx.String(ln.Ref.Value)).
		Cons(getAttributes(ln.Attrs)).
		Cons(mapGetS(mapRefStateLink, ln.Ref.State))
}

func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
	tail := t.getInlineList(en.Inlines)
	if en.Syntax == meta.SyntaxSVG {
		tail = tail.Cons(sx.String(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return tail.Cons(sx.String(en.Syntax)).Cons(getAttributes(en.Attrs)).Cons(sz.SymEmbedBLOB)
}

func (t *Transformer) getBlockList(bs *ast.BlockSlice) *sx.Pair {
	objs := make(sx.Vector, len(*bs))
	for i, n := range *bs {
		objs[i] = t.GetSz(n)
	}







|






|


















|







|



|







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
func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
	return t.getInlineList(cell.Inlines).Cons(mapGetS(alignmentSymbolS, cell.Align))
}

func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
	var lastObj sx.Object
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sx.MakeString(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sx.MakeList(
		sz.SymBLOB,
		t.getInlineList(bn.Description),
		sx.MakeString(bn.Syntax),
		lastObj,
	)
}

var mapRefStateLink = map[ast.RefState]*sx.Symbol{
	ast.RefStateInvalid:  sz.SymLinkInvalid,
	ast.RefStateZettel:   sz.SymLinkZettel,
	ast.RefStateSelf:     sz.SymLinkSelf,
	ast.RefStateFound:    sz.SymLinkFound,
	ast.RefStateBroken:   sz.SymLinkBroken,
	ast.RefStateHosted:   sz.SymLinkHosted,
	ast.RefStateBased:    sz.SymLinkBased,
	ast.RefStateQuery:    sz.SymLinkQuery,
	ast.RefStateExternal: sz.SymLinkExternal,
}

func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair {
	return t.getInlineList(ln.Inlines).
		Cons(sx.MakeString(ln.Ref.Value)).
		Cons(getAttributes(ln.Attrs)).
		Cons(mapGetS(mapRefStateLink, ln.Ref.State))
}

func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
	tail := t.getInlineList(en.Inlines)
	if en.Syntax == meta.SyntaxSVG {
		tail = tail.Cons(sx.MakeString(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return tail.Cons(sx.MakeString(en.Syntax)).Cons(getAttributes(en.Attrs)).Cons(sz.SymEmbedBLOB)
}

func (t *Transformer) getBlockList(bs *ast.BlockSlice) *sx.Pair {
	objs := make(sx.Vector, len(*bs))
	for i, n := range *bs {
		objs[i] = t.GetSz(n)
	}
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
func getAttributes(a attrs.Attributes) sx.Object {
	if a.IsEmpty() {
		return sx.Nil()
	}
	keys := a.Keys()
	objs := make(sx.Vector, 0, len(keys))
	for _, k := range keys {
		objs = append(objs, sx.Cons(sx.String(k), sx.String(a[k])))
	}
	return sx.MakeList(objs...)
}

var mapRefStateS = map[ast.RefState]*sx.Symbol{
	ast.RefStateInvalid:  sz.SymRefStateInvalid,
	ast.RefStateZettel:   sz.SymRefStateZettel,
	ast.RefStateSelf:     sz.SymRefStateSelf,
	ast.RefStateFound:    sz.SymRefStateFound,
	ast.RefStateBroken:   sz.SymRefStateBroken,
	ast.RefStateHosted:   sz.SymRefStateHosted,
	ast.RefStateBased:    sz.SymRefStateBased,
	ast.RefStateQuery:    sz.SymRefStateQuery,
	ast.RefStateExternal: sz.SymRefStateExternal,
}

func getReference(ref *ast.Reference) *sx.Pair {
	return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.String(ref.Value))
}

var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
	meta.TypeCredential:   sz.SymTypeCredential,
	meta.TypeEmpty:        sz.SymTypeEmpty,
	meta.TypeID:           sz.SymTypeID,
	meta.TypeIDSet:        sz.SymTypeIDSet,
	meta.TypeNumber:       sz.SymTypeNumber,
	meta.TypeString:       sz.SymTypeString,
	meta.TypeTagSet:       sz.SymTypeTagSet,
	meta.TypeTimestamp:    sz.SymTypeTimestamp,
	meta.TypeURL:          sz.SymTypeURL,
	meta.TypeWord:         sz.SymTypeWord,
	meta.TypeZettelmarkup: sz.SymTypeZettelmarkup,
}


func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	pairs := m.ComputedPairs()
	objs := make(sx.Vector, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setObjs := make(sx.Vector, len(setList))
			for i, val := range setList {
				setObjs[i] = sx.String(val)
			}
			obj = sx.MakeList(setObjs...)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			obj = t.getInlineList(is)
		} else {
			obj = sx.String(p.Value)
		}
		objs = append(objs, sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType))
	}
	return sx.MakeList(objs...).Cons(sz.SymMeta)
}

func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol {







|

















|
















>












|






|







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
func getAttributes(a attrs.Attributes) sx.Object {
	if a.IsEmpty() {
		return sx.Nil()
	}
	keys := a.Keys()
	objs := make(sx.Vector, 0, len(keys))
	for _, k := range keys {
		objs = append(objs, sx.Cons(sx.MakeString(k), sx.MakeString(a[k])))
	}
	return sx.MakeList(objs...)
}

var mapRefStateS = map[ast.RefState]*sx.Symbol{
	ast.RefStateInvalid:  sz.SymRefStateInvalid,
	ast.RefStateZettel:   sz.SymRefStateZettel,
	ast.RefStateSelf:     sz.SymRefStateSelf,
	ast.RefStateFound:    sz.SymRefStateFound,
	ast.RefStateBroken:   sz.SymRefStateBroken,
	ast.RefStateHosted:   sz.SymRefStateHosted,
	ast.RefStateBased:    sz.SymRefStateBased,
	ast.RefStateQuery:    sz.SymRefStateQuery,
	ast.RefStateExternal: sz.SymRefStateExternal,
}

func getReference(ref *ast.Reference) *sx.Pair {
	return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.MakeString(ref.Value))
}

var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
	meta.TypeCredential:   sz.SymTypeCredential,
	meta.TypeEmpty:        sz.SymTypeEmpty,
	meta.TypeID:           sz.SymTypeID,
	meta.TypeIDSet:        sz.SymTypeIDSet,
	meta.TypeNumber:       sz.SymTypeNumber,
	meta.TypeString:       sz.SymTypeString,
	meta.TypeTagSet:       sz.SymTypeTagSet,
	meta.TypeTimestamp:    sz.SymTypeTimestamp,
	meta.TypeURL:          sz.SymTypeURL,
	meta.TypeWord:         sz.SymTypeWord,
	meta.TypeZettelmarkup: sz.SymTypeZettelmarkup,
}

// GetMeta transforms the given metadata into a sx list.
func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	pairs := m.ComputedPairs()
	objs := make(sx.Vector, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setObjs := make(sx.Vector, len(setList))
			for i, val := range setList {
				setObjs[i] = sx.MakeString(val)
			}
			obj = sx.MakeList(setObjs...)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			obj = t.getInlineList(is)
		} else {
			obj = sx.MakeString(p.Value)
		}
		objs = append(objs, sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType))
	}
	return sx.MakeList(objs...).Cons(sz.SymMeta)
}

func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol {
403
404
405
406
407
408
409
410
411
412
413
	var sb strings.Builder
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sx.String(sb.String())
	}
	return sx.String("")
}







|

|

401
402
403
404
405
406
407
408
409
410
411
	var sb strings.Builder
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sx.MakeString(sb.String())
	}
	return sx.MakeString("")
}

Changes to encoder/textenc/textenc.go.

13
14
15
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 textenc encodes the abstract syntax tree into its text.
package textenc

import (
	"io"

	"zettelstore.de/client.fossil/api"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myTE }


type Encoder struct{}

var myTE Encoder // Only a singleton is required.

// WriteZettel writes metadata and content.
func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)







|
>












>







13
14
15
16
17
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 textenc encodes the abstract syntax tree into its text.
package textenc

import (
	"io"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myTE }

// Encoder contains all data needed for encoding.
type Encoder struct{}

var myTE Encoder // Only a singleton is required.

// WriteZettel writes metadata and content.
func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
68
69
70
71
72
73
74

75
76
77
78
79
80
81
			buf.WriteByte(' ')
		}
		buf.WriteString(meta.CleanTag(tag))
	}

}


func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return te.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)







>







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
			buf.WriteByte(' ')
		}
		buf.WriteString(meta.CleanTag(tag))
	}

}

// WriteContent encodes the zettel content.
func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return te.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
		return nil
	case *ast.TableNode:
		v.visitTable(n)
		return nil
	case *ast.TranscludeNode, *ast.BLOBNode:
		return nil
	case *ast.TextNode:
		v.b.WriteString(n.Text)
		return nil
	case *ast.SpaceNode:
		v.b.WriteByte(' ')
		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteByte('\n')
		} else {
			v.b.WriteByte(' ')
		}







|
<
<
<







131
132
133
134
135
136
137
138



139
140
141
142
143
144
145
		return nil
	case *ast.TableNode:
		v.visitTable(n)
		return nil
	case *ast.TranscludeNode, *ast.BLOBNode:
		return nil
	case *ast.TextNode:
		v.visitText(n.Text)



		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteByte('\n')
		} else {
			v.b.WriteByte(' ')
		}
226
227
228
229
230
231
232















233
234
235
236
237
238
func (v *visitor) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		v.inlinePos = i
		ast.Walk(v, in)
	}
	v.inlinePos = 0
}
















func (v *visitor) writePosChar(pos int, ch byte) {
	if pos > 0 {
		v.b.WriteByte(ch)
	}
}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
func (v *visitor) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		v.inlinePos = i
		ast.Walk(v, in)
	}
	v.inlinePos = 0
}

func (v *visitor) visitText(s string) {
	spaceFound := false
	for _, ch := range s {
		if input.IsSpace(ch) {
			if !spaceFound {
				v.b.WriteByte(' ')
				spaceFound = true
			}
			continue
		}
		spaceFound = false
		v.b.WriteString(string(ch))
	}
}

func (v *visitor) writePosChar(pos int, ch byte) {
	if pos > 0 {
		v.b.WriteByte(ch)
	}
}

Changes to encoder/zmkenc/zmkenc.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
package zmkenc

import (
	"fmt"
	"io"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myZE }


type Encoder struct{}

var myZE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)







|
|














>







15
16
17
18
19
20
21
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 zmkenc

import (
	"fmt"
	"io"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myZE }

// Encoder contains all data needed for encoding.
type Encoder struct{}

var myZE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
71
72
73
74
75
76
77

78
79
80
81
82
83
84
		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}


func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)







>







72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}

// WriteContent encodes the zettel content.
func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
	case *ast.TranscludeNode:
		v.b.WriteStrings("{{{", n.Ref.String(), "}}}")
		v.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.visitText(n)
	case *ast.SpaceNode:
		v.b.WriteString(n.Lexeme)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:







<
<







138
139
140
141
142
143
144


145
146
147
148
149
150
151
	case *ast.TranscludeNode:
		v.b.WriteStrings("{{{", n.Ref.String(), "}}}")
		v.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.visitText(n)


	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:

Deleted encoding/atom/atom.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package atom provides an Atom encoding.
package atom

import (
	"bytes"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const ContentType = "application/atom+xml"

type Configuration struct {
	Title            string
	Generator        string
	NewURLBuilderAbs func() *api.URLBuilder
}

func (c *Configuration) Setup(cfg config.Config) {
	baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)

	c.Title = cfg.GetSiteName()
	c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
		" " +
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
	c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
}

func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
	atomUpdated := encoding.LastUpdated(ml, time.RFC3339)
	feedLink := c.NewURLBuilderAbs().String()

	var buf bytes.Buffer
	buf.WriteString(`<feed xmlns="http://www.w3.org/2005/Atom">` + "\n")
	xml.WriteTag(&buf, "  ", "title", c.Title)
	xml.WriteTag(&buf, "  ", "id", feedLink)
	buf.WriteString(`  <link rel="self" href="`)
	if s := q.String(); s != "" {
		strfun.XMLEscape(&buf, c.NewURLBuilderAbs().AppendQuery(s).String())
	} else {
		strfun.XMLEscape(&buf, feedLink)
	}
	buf.WriteString(`"/>` + "\n")
	if atomUpdated != "" {
		xml.WriteTag(&buf, "  ", "updated", atomUpdated)
	}
	xml.WriteTag(&buf, "  ", "generator", c.Generator)
	buf.WriteString("  <author><name>Unknown</name></author>\n")

	for _, m := range ml {
		c.marshalMeta(&buf, m)
	}

	buf.WriteString("</feed>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	entryUpdated := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
			entryUpdated = published.UTC().Format(time.RFC3339)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).String()

	buf.WriteString("  <entry>\n")
	xml.WriteTag(buf, "    ", "title", encoding.TitleAsText(m))
	xml.WriteTag(buf, "    ", "id", link)
	buf.WriteString(`    <link rel="self" href="`)
	strfun.XMLEscape(buf, link)
	buf.WriteString(`"/>` + "\n")
	buf.WriteString(`    <link rel="alternate" type="text/html" href="`)
	strfun.XMLEscape(buf, link)
	buf.WriteString(`"/>` + "\n")

	if entryUpdated != "" {
		xml.WriteTag(buf, "    ", "updated", entryUpdated)
	}
	marshalTags(buf, m)
	buf.WriteString("  </entry>\n")
}

func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
	if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
		for _, tag := range tags {
			for len(tag) > 0 && tag[0] == '#' {
				tag = tag[1:]
			}
			if tag != "" {
				buf.WriteString(`    <category term="`)
				strfun.XMLEscape(buf, tag)
				buf.WriteString("\"/>\n")
			}
		}
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































































Deleted encoding/encoding.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package encoding provides helper functions for encodings.
package encoding

import (
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
	maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local)
	for _, m := range ml {
		if val, found := m.Get(api.KeyPublished); found {
			if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
				if maxPublished.Before(published) {
					maxPublished = published
				}
			}
		}
	}
	if maxPublished.Year() > 1 {
		return maxPublished.UTC().Format(timeFormat)
	}
	return ""
}

// TitleAsText returns the title of a zettel as plain text
func TitleAsText(m *meta.Meta) string { return parser.NormalizedSpacedText(m.GetTitle()) }
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


























































































Deleted encoding/rss/rss.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package rss provides a RSS encoding.
package rss

import (
	"bytes"
	"context"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const ContentType = "application/rss+xml"

type Configuration struct {
	Title            string
	Language         string
	Copyright        string
	Generator        string
	NewURLBuilderAbs func() *api.URLBuilder
}

func (c *Configuration) Setup(ctx context.Context, cfg config.Config) {
	baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)
	defVals := cfg.AddDefaultValues(ctx, &meta.Meta{})

	c.Title = cfg.GetSiteName()
	c.Language = defVals.GetDefault(api.KeyLang, "")
	c.Copyright = defVals.GetDefault(api.KeyCopyright, "")
	c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
		" " +
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
	c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
}

func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
	rssPublished := encoding.LastUpdated(ml, time.RFC1123Z)

	atomLink := ""
	if s := q.String(); s != "" {
		atomLink = c.NewURLBuilderAbs().AppendQuery(s).String()
	}
	var buf bytes.Buffer
	buf.WriteString(`<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">` + "\n<channel>\n")
	xml.WriteTag(&buf, "  ", "title", c.Title)
	xml.WriteTag(&buf, "  ", "link", c.NewURLBuilderAbs().String())
	xml.WriteTag(&buf, "  ", "description", "")
	xml.WriteTag(&buf, "  ", "language", c.Language)
	xml.WriteTag(&buf, "  ", "copyright", c.Copyright)
	if rssPublished != "" {
		xml.WriteTag(&buf, "  ", "pubDate", rssPublished)
		xml.WriteTag(&buf, "  ", "lastBuildDate", rssPublished)
	}
	xml.WriteTag(&buf, "  ", "generator", c.Generator)
	buf.WriteString("  <docs>https://www.rssboard.org/rss-specification</docs>\n")
	if atomLink != "" {
		buf.WriteString(`  <atom:link href="`)
		strfun.XMLEscape(&buf, atomLink)
		buf.WriteString(`" rel="self" type="application/rss+xml"></atom:link>` + "\n")
	}
	for _, m := range ml {
		c.marshalMeta(&buf, m)
	}

	buf.WriteString("</channel>\n</rss>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	itemPublished := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
			itemPublished = published.UTC().Format(time.RFC1123Z)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).String()

	buf.WriteString("  <item>\n")
	xml.WriteTag(buf, "    ", "title", encoding.TitleAsText(m))
	xml.WriteTag(buf, "    ", "link", link)
	xml.WriteTag(buf, "    ", "guid", link)
	if itemPublished != "" {
		xml.WriteTag(buf, "    ", "pubDate", itemPublished)
	}
	marshalTags(buf, m)
	buf.WriteString("  </item>\n")
}

func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
	if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
		for _, tag := range tags {
			for len(tag) > 0 && tag[0] == '#' {
				tag = tag[1:]
			}
			if tag != "" {
				xml.WriteTag(buf, "    ", "category", tag)
			}
		}
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































Deleted encoding/xml/xml.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package xml provides helper for a XML-based encoding.
package xml

import (
	"bytes"

	"zettelstore.de/z/strfun"
)

// Header contains the string that should start all XML documents.
const Header = `<?xml version="1.0" encoding="UTF-8"?>` + "\n"

// WriteTag writes a simple XML tag with a given prefix and a specific value.
func WriteTag(buf *bytes.Buffer, prefix, tag, value string) {
	buf.WriteString(prefix)
	buf.WriteByte('<')
	buf.WriteString(tag)
	buf.WriteByte('>')
	strfun.XMLEscape(buf, value)
	buf.WriteString("</")
	buf.WriteString(tag)
	buf.WriteString(">\n")
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































Changes to evaluator/evaluator.go.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"context"
	"errors"
	"fmt"
	"path"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/sx.fossil/sxbuiltins"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/parser/draw"
	"zettelstore.de/z/query"







|
|
|
|
|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"context"
	"errors"
	"fmt"
	"path"
	"strconv"
	"strings"

	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/parser/draw"
	"zettelstore.de/z/query"
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
	ml, err := e.port.QueryMeta(e.ctx, q)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
	}
	result, _ := QueryAction(e.ctx, q, ml, e.rtConfig)
	if result != nil {
		ast.Walk(e, result)
	}
	return result
}

func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {







|







292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
	ml, err := e.port.QueryMeta(e.ctx, q)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
	}
	result, _ := QueryAction(e.ctx, q, ml)
	if result != nil {
		ast.Walk(e, result)
	}
	return result
}

func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {
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
		ast.Walk(fs, bn)
	}
}

func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
			ris := skipSpaceNodes((*is)[i+1:])
			if len(mn.Inlines) > 0 {
				fs.result = append(ast.InlineSlice{}, mn.Inlines...)
				fs.result = append(fs.result, &ast.SpaceNode{Lexeme: " "})
				fs.result = append(fs.result, ris...)
			} else {
				fs.result = ris
			}
			return
		}
		ast.Walk(fs, in)
	}
}

func skipSpaceNodes(ins ast.InlineSlice) ast.InlineSlice {
	for i, in := range ins {
		switch in.(type) {
		case *ast.SpaceNode:
		case *ast.BreakNode:
		default:
			return ins[i:]
		}
	}
	return nil
}







|


|










|


<







631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654

655
656
657
658
659
660
661
		ast.Walk(fs, bn)
	}
}

func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
			ris := skipBreakeNodes((*is)[i+1:])
			if len(mn.Inlines) > 0 {
				fs.result = append(ast.InlineSlice{}, mn.Inlines...)
				fs.result = append(fs.result, &ast.TextNode{Text: " "})
				fs.result = append(fs.result, ris...)
			} else {
				fs.result = ris
			}
			return
		}
		ast.Walk(fs, in)
	}
}

func skipBreakeNodes(ins ast.InlineSlice) ast.InlineSlice {
	for i, in := range ins {
		switch in.(type) {

		case *ast.BreakNode:
		default:
			return ins[i:]
		}
	}
	return nil
}

Changes to evaluator/list.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

package evaluator

import (
	"bytes"
	"context"
	"math"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// QueryAction transforms a list of metadata according to query actions into a AST nested list.
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) (ast.BlockNode, int) {
	ap := actionPara{
		ctx:   ctx,
		q:     q,
		ml:    ml,
		kind:  ast.NestedListUnordered,
		min:   -1,
		max:   -1,
		title: rtConfig.GetSiteName(),
	}
	actions := q.Actions()
	if len(actions) == 0 {
		return ap.createBlockNodeMeta("")
	}

	acts := make([]string, 0, len(actions))
	for i, act := range actions {
		if strings.HasPrefix(act, api.NumberedAction[0:1]) {
			ap.kind = ast.NestedListOrdered
			continue
		}
		if strings.HasPrefix(act, api.MinAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.min = num
				continue
			}
		}
		if strings.HasPrefix(act, api.MaxAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.max = num
				continue
			}
		}
		if act == api.TitleAction && i+1 < len(actions) {
			ap.title = strings.Join(actions[i+1:], " ")
			break
		}
		if act == api.ReIndexAction {
			continue
		}
		acts = append(acts, act)
	}
	var firstUnknowAct string
	for _, act := range acts {
		switch act {
		case api.AtomAction:
			return ap.createBlockNodeAtom(rtConfig)
		case api.RSSAction:
			return ap.createBlockNodeRSS(rtConfig)
		case api.KeysAction:
			return ap.createBlockNodeMetaKeys()
		}
		key := strings.ToLower(act)
		switch meta.Type(key) {
		case meta.TypeWord:
			return ap.createBlockNodeWord(key)







|



|
|

<
<
<






|

|
|
|
|
|
|
<







|






|





|



<
<
<
<








<
<
<
<







13
14
15
16
17
18
19
20
21
22
23
24
25
26



27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 evaluator

import (
	"bytes"
	"context"
	"math"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"zettelstore.de/z/ast"



	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// QueryAction transforms a list of metadata according to query actions into a AST nested list.
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (ast.BlockNode, int) {
	ap := actionPara{
		ctx:    ctx,
		q:      q,
		ml:     ml,
		kind:   ast.NestedListUnordered,
		minVal: -1,
		maxVal: -1,

	}
	actions := q.Actions()
	if len(actions) == 0 {
		return ap.createBlockNodeMeta("")
	}

	acts := make([]string, 0, len(actions))
	for _, act := range actions {
		if strings.HasPrefix(act, api.NumberedAction[0:1]) {
			ap.kind = ast.NestedListOrdered
			continue
		}
		if strings.HasPrefix(act, api.MinAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.minVal = num
				continue
			}
		}
		if strings.HasPrefix(act, api.MaxAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.maxVal = num
				continue
			}
		}




		if act == api.ReIndexAction {
			continue
		}
		acts = append(acts, act)
	}
	var firstUnknowAct string
	for _, act := range acts {
		switch act {




		case api.KeysAction:
			return ap.createBlockNodeMetaKeys()
		}
		key := strings.ToLower(act)
		switch meta.Type(key) {
		case meta.TypeWord:
			return ap.createBlockNodeWord(key)
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
	if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) {
		bn, numItems = ap.createBlockNodeMeta("")
	}
	return bn, numItems
}

type actionPara struct {
	ctx   context.Context
	q     *query.Query
	ml    []*meta.Meta
	kind  ast.NestedListKind
	min   int
	max   int
	title string
}

func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		return nil, 0







|
|
|
|
|
|
<







88
89
90
91
92
93
94
95
96
97
98
99
100

101
102
103
104
105
106
107
	if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) {
		bn, numItems = ap.createBlockNodeMeta("")
	}
	return bn, numItems
}

type actionPara struct {
	ctx    context.Context
	q      *query.Query
	ml     []*meta.Meta
	kind   ast.NestedListKind
	minVal int
	maxVal int

}

func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		return nil, 0
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
	ccs = ap.limitTags(ccs)
	countMap := ap.calcFontSizes(ccs)

	para := make(ast.InlineSlice, 0, len(ccs))
	ccs.SortByName()
	for i, cat := range ccs {
		if i > 0 {
			para = append(para, &ast.SpaceNode{Lexeme: " "})
		}
		buf.WriteString(cat.Name)
		para = append(para,
			&ast.LinkNode{
				Attrs: countMap[cat.Count],
				Ref:   ast.ParseReference(buf.String()),
				Inlines: ast.InlineSlice{







|







134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
	ccs = ap.limitTags(ccs)
	countMap := ap.calcFontSizes(ccs)

	para := make(ast.InlineSlice, 0, len(ccs))
	ccs.SortByName()
	for i, cat := range ccs {
		if i > 0 {
			para = append(para, &ast.TextNode{Text: " "})
		}
		buf.WriteString(cat.Name)
		para = append(para,
			&ast.LinkNode{
				Attrs: countMap[cat.Count],
				Ref:   ast.ParseReference(buf.String()),
				Inlines: ast.InlineSlice{
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
		)
		buf.Truncate(bufLen)
	}
	return &ast.ParaNode{Inlines: para}, len(ccs)
}

func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories {
	if min, max := ap.min, ap.max; min > 0 || max > 0 {
		if min < 0 {
			min = ccs[len(ccs)-1].Count
		}
		if max < 0 {
			max = ccs[0].Count
		}
		if ccs[len(ccs)-1].Count < min || max < ccs[0].Count {
			temp := make(meta.CountedCategories, 0, len(ccs))
			for _, cat := range ccs {
				if min <= cat.Count && cat.Count <= max {
					temp = append(temp, cat)
				}
			}
			return temp
		}
	}
	return ccs







|
|
|

|
|

|


|







157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
		)
		buf.Truncate(bufLen)
	}
	return &ast.ParaNode{Inlines: para}, len(ccs)
}

func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories {
	if minVal, maxVal := ap.minVal, ap.maxVal; minVal > 0 || maxVal > 0 {
		if minVal < 0 {
			minVal = ccs[len(ccs)-1].Count
		}
		if maxVal < 0 {
			maxVal = ccs[0].Count
		}
		if ccs[len(ccs)-1].Count < minVal || maxVal < ccs[0].Count {
			temp := make(meta.CountedCategories, 0, len(ccs))
			for _, cat := range ccs {
				if minVal <= cat.Count && cat.Count <= maxVal {
					temp = append(temp, cat)
				}
			}
			return temp
		}
	}
	return ccs
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

		items = append(items, ast.ItemSlice{ast.CreateParaNode(
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q1),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
			},
			&ast.SpaceNode{Lexeme: " "},
			&ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "},
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q2),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}},
			},
			&ast.TextNode{Text: ")"},







|







209
210
211
212
213
214
215
216
217
218
219
220
221
222
223

		items = append(items, ast.ItemSlice{ast.CreateParaNode(
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q1),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
			},
			&ast.TextNode{Text: " "},
			&ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "},
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q2),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}},
			},
			&ast.TextNode{Text: ")"},
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
		countMap[cat.Count]++
	}

	countList := make([]int, 0, len(countMap))
	for count := range countMap {
		countList = append(countList, count)
	}
	sort.Ints(countList)

	result := make(map[int]attrs.Attributes, len(countList))
	if len(countList) <= fontSizes {
		// If we have less different counts, center them inside the fsAttrs vector.
		curSize := (fontSizes - len(countList)) / 2
		for _, count := range countList {
			result[count] = fsAttrs[curSize]







|







297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
		countMap[cat.Count]++
	}

	countList := make([]int, 0, len(countMap))
	for count := range countMap {
		countList = append(countList, count)
	}
	slices.Sort(countList)

	result := make(map[int]attrs.Attributes, len(countList))
	if len(countList) <= fontSizes {
		// If we have less different counts, center them inside the fsAttrs vector.
		curSize := (fontSizes - len(countList)) / 2
		for _, count := range countList {
			result[count] = fsAttrs[curSize]
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
			}
		}
	}
	return result
}

func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) }

func (ap *actionPara) createBlockNodeRSS(cfg config.Config) (ast.BlockNode, int) {
	var rssConfig rss.Configuration
	rssConfig.Setup(ap.ctx, cfg)
	rssConfig.Title = ap.title
	data := rssConfig.Marshal(ap.q, ap.ml)

	return &ast.VerbatimNode{
		Kind:    ast.VerbatimProg,
		Attrs:   attrs.Attributes{"lang": "xml"},
		Content: data,
	}, len(ap.ml)
}

func (ap *actionPara) createBlockNodeAtom(cfg config.Config) (ast.BlockNode, int) {
	var atomConfig atom.Configuration
	atomConfig.Setup(cfg)
	atomConfig.Title = ap.title
	data := atomConfig.Marshal(ap.q, ap.ml)

	return &ast.VerbatimNode{
		Kind:    ast.VerbatimProg,
		Attrs:   attrs.Attributes{"lang": "xml"},
		Content: data,
	}, len(ap.ml)
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
333
334
335
336
337
338
339


























			}
		}
	}
	return result
}

func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) }


























Changes to evaluator/metadata.go.

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
		sliceData = []string{value}
	}
	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet

	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.SpaceNode{Lexeme: " "})
		}
		tn := &ast.TextNode{Text: val}
		if makeLink {
			result = append(result, &ast.LinkNode{
				Ref:     ast.ParseReference(val),
				Inlines: ast.InlineSlice{tn},
			})







|







46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
		sliceData = []string{value}
	}
	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet

	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.TextNode{Text: " "})
		}
		tn := &ast.TextNode{Text: val}
		if makeLink {
			result = append(result, &ast.LinkNode{
				Ref:     ast.ParseReference(val),
				Inlines: ast.InlineSlice{tn},
			})

Changes to go.mod.

1
2
3
4
5
6
7
8
9
10
11

12
13
14

15


module zettelstore.de/z

go 1.22

require (
	github.com/fsnotify/fsnotify v1.7.0
	github.com/yuin/goldmark v1.7.0
	golang.org/x/crypto v0.20.0
	golang.org/x/term v0.17.0
	golang.org/x/text v0.14.0
	zettelstore.de/client.fossil v0.0.0-20240304164340-1f9d9b832cdd

	zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d
)


require golang.org/x/sys v0.17.0 // indirect




|


|
|
|
|
|
|
>
|


>
|
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module zettelstore.de/z

go 1.23

require (
	github.com/fsnotify/fsnotify v1.8.0
	github.com/yuin/goldmark v1.7.8
	golang.org/x/crypto v0.31.0
	golang.org/x/term v0.27.0
	golang.org/x/text v0.21.0
	t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5
	t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f
	t73f.de/r/zsc v0.0.0-20241223132707-d07b16e75819
)

require (
	golang.org/x/sys v0.28.0 // indirect
	t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd // indirect
)

Changes to go.sum.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16




github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
zettelstore.de/client.fossil v0.0.0-20240304164340-1f9d9b832cdd h1:+LUJqi1mvXo/zM9Ii64hcGd1LD3oC8kh5yrmw2fFoco=
zettelstore.de/client.fossil v0.0.0-20240304164340-1f9d9b832cdd/go.mod h1:y5zhvVuDHJKFcySEe70537w+5RL50jpeZjqyQuBjfa0=
zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d h1:Gl5ZmdNV5wJsNMIQYjAd/sWLq2ng4NP+eglWU7lQP+I=
zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d/go.mod h1:/iGHxFXoo6GSV04PUkwaLuFrrCa5LMorxD73iLMAruI=




|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 h1:ug4hohM6pK28M8Uo0o3+XvjBure2wfEtuCnHVIdqBZY=
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5/go.mod h1:VRvsWoBErPKvMieDMMk1hsh1tb9sA4ijEQWGw/TbtQ0=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f h1:VJ4S7YWy9tCJuFz5MckqUjjktPaf0kpnTkNBVRVXpo4=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f/go.mod h1:IaM+U+LvYTYeuiIS5cwZW6kcEpdwoKBYVCU7LZr4Sgk=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd h1:+7cqJonXKDso+uPvsvOPl7BiLkhj8VQT/Has8qC5VIQ=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd/go.mod h1:NSoOON8be62MfQZzlCApK27Jt2zhIa6Vrmo9RJ4tOnQ=
t73f.de/r/zsc v0.0.0-20241223132707-d07b16e75819 h1:PAdoF/SOC0bMY1BrQyILWCh7lygtK24c2NWafKiqpIM=
t73f.de/r/zsc v0.0.0-20241223132707-d07b16e75819/go.mod h1:sQWKzNp0I18aSFnKJjAyhL1zTWITIj1v2acWv4GvuIY=

Changes to kernel/impl/cfg.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"







|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},
		kernel.ConfigSimpleMode:        {"Simple mode", cs.noFrozen(parseBool), true},
		config.KeyShowBackLinks:        {"Show back links", parseString, true},
		config.KeyShowFolgeLinks:       {"Show folge links", parseString, true},
		config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true},
		config.KeyShowSuccessorLinks:   {"Show successor links", parseString, true},
	}
	cs.next = interfaceMap{
		keyDefaultCopyright:            "",
		keyDefaultLicense:              "",
		keyDefaultVisibility:           meta.VisibilityLogin,
		keyExpertMode:                  false,
		config.KeyFooterZettel:         id.Invalid,
		config.KeyHomeZettel:           id.DefaultHomeZid,
		kernel.ConfigInsecureHTML:      config.NoHTML,
		api.KeyLang:                    api.ValueLangEN,
		keyMaxTransclusions:            int64(1024),
		keySiteName:                    "Zettelstore",
		keyYAMLHeader:                  false,
		keyZettelFileSyntax:            nil,
		kernel.ConfigSimpleMode:        false,
		config.KeyShowBackLinks:        "",
		config.KeyShowFolgeLinks:       "",
		config.KeyShowSubordinateLinks: "",
		config.KeyShowSuccessorLinks:   "",
	}
}
func (cs *configService) GetLogger() *logger.Logger { return cs.logger }

func (cs *configService) Start(*myKernel) error {
	cs.logger.Info().Msg("Start Service")
	data := meta.New(id.ConfigurationZid)







|
|
|
|
|


|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},
		kernel.ConfigSimpleMode:      {"Simple mode", cs.noFrozen(parseBool), true},
		config.KeyShowBackLinks:      {"Show back links", parseString, true},
		config.KeyShowFolgeLinks:     {"Show folge links", parseString, true},
		config.KeyShowSequelLinks:    {"Show sequel links", parseString, true},
		config.KeyShowSuccessorLinks: {"Show successor links", parseString, true},
	}
	cs.next = interfaceMap{
		keyDefaultCopyright:          "",
		keyDefaultLicense:            "",
		keyDefaultVisibility:         meta.VisibilityLogin,
		keyExpertMode:                false,
		config.KeyFooterZettel:       id.Invalid,
		config.KeyHomeZettel:         id.DefaultHomeZid,
		kernel.ConfigInsecureHTML:    config.NoHTML,
		api.KeyLang:                  api.ValueLangEN,
		keyMaxTransclusions:          int64(1024),
		keySiteName:                  "Zettelstore",
		keyYAMLHeader:                false,
		keyZettelFileSyntax:          nil,
		kernel.ConfigSimpleMode:      false,
		config.KeyShowBackLinks:      "",
		config.KeyShowFolgeLinks:     "",
		config.KeyShowSequelLinks:    "",
		config.KeyShowSuccessorLinks: "",
	}
}
func (cs *configService) GetLogger() *logger.Logger { return cs.logger }

func (cs *configService) Start(*myKernel) error {
	cs.logger.Info().Msg("Start Service")
	data := meta.New(id.ConfigurationZid)
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() {
			cs.mxService.RLock()
			mgr := cs.manager
			cs.mxService.RUnlock()
			if mgr != nil {
				cs.doUpdate(mgr)







|







180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ConfigurationZid {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() {
			cs.mxService.RLock()
			mgr := cs.manager
			cs.mxService.RUnlock()
			if mgr != nil {
				cs.doUpdate(mgr)

Changes to kernel/impl/cmd.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package impl

import (
	"fmt"
	"io"
	"os"
	"runtime/metrics"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
)

type cmdSession struct {
	w        io.Writer







|



|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package impl

import (
	"fmt"
	"io"
	"os"
	"runtime/metrics"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
)

type cmdSession struct {
	w        io.Writer
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
	"bye": {
		"end this session",
		func(*cmdSession, string, []string) bool { return false },
	},
	"config": {"show configuration keys", cmdConfig},
	"crlf": {
		"toggle crlf mode",
		func(sess *cmdSession, cmd string, args []string) bool {
			if len(sess.eol) == 1 {
				sess.eol = []byte{'\r', '\n'}
				sess.println("crlf is on")
			} else {
				sess.eol = []byte{'\n'}
				sess.println("crlf is off")
			}
			return true
		},
	},
	"dump-index":   {"writes the content of the index", cmdDumpIndex},
	"dump-recover": {"show data of last recovery", cmdDumpRecover},
	"echo": {
		"toggle echo mode",
		func(sess *cmdSession, cmd string, args []string) bool {
			sess.echo = !sess.echo
			if sess.echo {
				sess.println("echo is on")
			} else {
				sess.println("echo is off")
			}
			return true
		},
	},
	"end-profile": {"stop profiling", cmdEndProfile},
	"env":         {"show environment values", cmdEnvironment},
	"get-config":  {"show current configuration data", cmdGetConfig},
	"header": {
		"toggle table header",
		func(sess *cmdSession, cmd string, args []string) bool {
			sess.header = !sess.header
			if sess.header {
				sess.println("header are on")
			} else {
				sess.println("header are off")
			}
			return true
		},
	},
	"log-level":   {"get/set log level", cmdLogLevel},
	"metrics":     {"show Go runtime metrics", cmdMetrics},
	"next-config": {"show next configuration data", cmdNextConfig},
	"profile":     {"start profiling", cmdProfile},
	"refresh":     {"refresh box data", cmdRefresh},
	"restart":     {"restart service", cmdRestart},
	"services":    {"show available services", cmdServices},
	"set-config":  {"set next configuration data", cmdSetConfig},
	"shutdown": {
		"shutdown Zettelstore",
		func(sess *cmdSession, cmd string, args []string) bool { sess.kern.Shutdown(false); return false },
	},
	"start": {"start service", cmdStart},
	"stat":  {"show service statistics", cmdStat},
	"stop":  {"stop service", cmdStop},
}

func cmdHelp(sess *cmdSession, _ string, _ []string) bool {







|














|














|



















|







140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
	"bye": {
		"end this session",
		func(*cmdSession, string, []string) bool { return false },
	},
	"config": {"show configuration keys", cmdConfig},
	"crlf": {
		"toggle crlf mode",
		func(sess *cmdSession, _ string, _ []string) bool {
			if len(sess.eol) == 1 {
				sess.eol = []byte{'\r', '\n'}
				sess.println("crlf is on")
			} else {
				sess.eol = []byte{'\n'}
				sess.println("crlf is off")
			}
			return true
		},
	},
	"dump-index":   {"writes the content of the index", cmdDumpIndex},
	"dump-recover": {"show data of last recovery", cmdDumpRecover},
	"echo": {
		"toggle echo mode",
		func(sess *cmdSession, _ string, _ []string) bool {
			sess.echo = !sess.echo
			if sess.echo {
				sess.println("echo is on")
			} else {
				sess.println("echo is off")
			}
			return true
		},
	},
	"end-profile": {"stop profiling", cmdEndProfile},
	"env":         {"show environment values", cmdEnvironment},
	"get-config":  {"show current configuration data", cmdGetConfig},
	"header": {
		"toggle table header",
		func(sess *cmdSession, _ string, _ []string) bool {
			sess.header = !sess.header
			if sess.header {
				sess.println("header are on")
			} else {
				sess.println("header are off")
			}
			return true
		},
	},
	"log-level":   {"get/set log level", cmdLogLevel},
	"metrics":     {"show Go runtime metrics", cmdMetrics},
	"next-config": {"show next configuration data", cmdNextConfig},
	"profile":     {"start profiling", cmdProfile},
	"refresh":     {"refresh box data", cmdRefresh},
	"restart":     {"restart service", cmdRestart},
	"services":    {"show available services", cmdServices},
	"set-config":  {"set next configuration data", cmdSetConfig},
	"shutdown": {
		"shutdown Zettelstore",
		func(sess *cmdSession, _ string, _ []string) bool { sess.kern.Shutdown(false); return false },
	},
	"start": {"start service", cmdStart},
	"stat":  {"show service statistics", cmdStat},
	"stop":  {"stop service", cmdStop},
}

func cmdHelp(sess *cmdSession, _ string, _ []string) bool {
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
	listConfig func(*cmdSession, service), getConfig func(service, string) interface{}) {

	if len(args) == 0 {
		keys := make([]int, 0, len(sess.kern.srvs))
		for k := range sess.kern.srvs {
			keys = append(keys, int(k))
		}
		sort.Ints(keys)
		for i, k := range keys {
			if i > 0 {
				sess.println()
			}
			srvD := sess.kern.srvs[kernel.Service(k)]
			sess.println("%% Service", srvD.name)
			listConfig(sess, srvD.srv)







|







238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
	listConfig func(*cmdSession, service), getConfig func(service, string) interface{}) {

	if len(args) == 0 {
		keys := make([]int, 0, len(sess.kern.srvs))
		for k := range sess.kern.srvs {
			keys = append(keys, int(k))
		}
		slices.Sort(keys)
		for i, k := range keys {
			if i > 0 {
				sess.println()
			}
			srvD := sess.kern.srvs[kernel.Service(k)]
			sess.println("%% Service", srvD.name)
			listConfig(sess, srvD.srv)
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
	if err != nil {
		sess.println(err.Error())
	}
	return true
}

func cmdStop(sess *cmdSession, cmd string, args []string) bool {
	srvnum, ok := lookupService(sess, cmd, args)
	if !ok {
		return true
	}
	err := sess.kern.doStopService(srvnum)
	if err != nil {
		sess.println(err.Error())
	}
	return true
}

func cmdStat(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.usage(cmd, "SERVICE")







|
<
<
<
|
<
<







337
338
339
340
341
342
343
344



345


346
347
348
349
350
351
352
	if err != nil {
		sess.println(err.Error())
	}
	return true
}

func cmdStop(sess *cmdSession, cmd string, args []string) bool {
	if srvnum, ok := lookupService(sess, cmd, args); ok {



		sess.kern.doStopService(srvnum)


	}
	return true
}

func cmdStat(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.usage(cmd, "SERVICE")
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
		workDir = err.Error()
	}
	execName, err := os.Executable()
	if err != nil {
		execName = err.Error()
	}
	envs := os.Environ()
	sort.Strings(envs)

	table := [][]string{
		{"Key", "Value"},
		{"workdir", workDir},
		{"executable", execName},
	}
	for _, env := range envs {







|







536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
		workDir = err.Error()
	}
	execName, err := os.Executable()
	if err != nil {
		execName = err.Error()
	}
	envs := os.Environ()
	slices.Sort(envs)

	table := [][]string{
		{"Key", "Value"},
		{"workdir", workDir},
		{"executable", execName},
	}
	for _, env := range envs {

Changes to kernel/impl/config.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//-----------------------------------------------------------------------------

package impl

import (
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

type parseFunc func(string) (any, error)
type configDescription struct {







|




|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//-----------------------------------------------------------------------------

package impl

import (
	"errors"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"sync"

	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

type parseFunc func(string) (any, error)
type configDescription struct {
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
				if val == nil {
					break
				}
				keys = append(keys, key)
			}
		}
	}
	sort.Strings(keys)
	return keys
}

func (cfg *srvConfig) Freeze() {
	cfg.mxConfig.Lock()
	cfg.frozen = true
	cfg.mxConfig.Unlock()







|







197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
				if val == nil {
					break
				}
				keys = append(keys, key)
			}
		}
	}
	slices.Sort(keys)
	return keys
}

func (cfg *srvConfig) Freeze() {
	cfg.mxConfig.Lock()
	cfg.frozen = true
	cfg.mxConfig.Unlock()
229
230
231
232
233
234
235
236

237
238
239
240

241
242
243
244

245
246
247
248

249
250
251
252
253
254
	case '0', 'f', 'F', 'n', 'N':
		return false, nil
	}
	return true, nil
}

func parseInt64(val string) (any, error) {
	if u64, err := strconv.ParseInt(val, 10, 64); err == nil {

		return u64, nil
	} else {
		return nil, err
	}

}

func parseZid(val string) (any, error) {
	if zid, err := id.Parse(val); err == nil {

		return zid, nil
	} else {
		return id.Invalid, err
	}

}

func parseInvalidZid(val string) (any, error) {
	zid, _ := id.Parse(val)
	return zid, nil
}







|
>

<
<

>



|
>

<
<

>






229
230
231
232
233
234
235
236
237
238


239
240
241
242
243
244
245
246


247
248
249
250
251
252
253
254
	case '0', 'f', 'F', 'n', 'N':
		return false, nil
	}
	return true, nil
}

func parseInt64(val string) (any, error) {
	u64, err := strconv.ParseInt(val, 10, 64)
	if err == nil {
		return u64, nil


	}
	return nil, err
}

func parseZid(val string) (any, error) {
	zid, err := id.Parse(val)
	if err == nil {
		return zid, nil


	}
	return id.Invalid, err
}

func parseInvalidZid(val string) (any, error) {
	zid, _ := id.Parse(val)
	return zid, nil
}

Changes to kernel/impl/core.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"fmt"
	"net"
	"os"
	"runtime"
	"sync"
	"time"

	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type coreService struct {







|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"fmt"
	"net"
	"os"
	"runtime"
	"sync"
	"time"

	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type coreService struct {

Changes to kernel/impl/impl.go.

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
			return err
		}
		srv.SwitchNextToCur()
	}
	return nil
}

func (kern *myKernel) StopService(srvnum kernel.Service) error {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	return kern.doStopService(srvnum)
}

func (kern *myKernel) doStopService(srvnum kernel.Service) error {
	for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) {
		srv.Stop(kern)
	}
	return nil
}

func (kern *myKernel) sortDependency(
	srvnum kernel.Service,
	srvdeps serviceDependency,
	isStarted bool,
) []service {







|


|


|



<







442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458

459
460
461
462
463
464
465
			return err
		}
		srv.SwitchNextToCur()
	}
	return nil
}

func (kern *myKernel) StopService(srvnum kernel.Service) {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	kern.doStopService(srvnum)
}

func (kern *myKernel) doStopService(srvnum kernel.Service) {
	for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) {
		srv.Stop(kern)
	}

}

func (kern *myKernel) sortDependency(
	srvnum kernel.Service,
	srvdeps serviceDependency,
	isStarted bool,
) []service {
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568

// --- The kernel as a service -------------------------------------------

type kernelService struct {
	kernel *myKernel
}

func (*kernelService) Initialize(*logger.Logger)                        {}
func (ks *kernelService) GetLogger() *logger.Logger                     { return ks.kernel.logger }
func (*kernelService) ConfigDescriptions() []serviceConfigDescription   { return nil }
func (*kernelService) SetConfig(key, value string) error                { return errAlreadyFrozen }
func (*kernelService) GetCurConfig(key string) interface{}              { return nil }
func (*kernelService) GetNextConfig(key string) interface{}             { return nil }
func (*kernelService) GetCurConfigList(all bool) []kernel.KeyDescrValue { return nil }
func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue        { return nil }
func (*kernelService) GetStatistics() []kernel.KeyValue                 { return nil }
func (*kernelService) Freeze()                                          {}
func (*kernelService) Start(*myKernel) error                            { return nil }
func (*kernelService) SwitchNextToCur()                                 {}
func (*kernelService) IsStarted() bool                                  { return true }
func (*kernelService) Stop(*myKernel)                                   {}







|
|
|
|
|
|
|
|
|
|
|
|
|
|
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567

// --- The kernel as a service -------------------------------------------

type kernelService struct {
	kernel *myKernel
}

func (*kernelService) Initialize(*logger.Logger)                      {}
func (ks *kernelService) GetLogger() *logger.Logger                   { return ks.kernel.logger }
func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil }
func (*kernelService) SetConfig(string, string) error                 { return errAlreadyFrozen }
func (*kernelService) GetCurConfig(string) interface{}                { return nil }
func (*kernelService) GetNextConfig(string) interface{}               { return nil }
func (*kernelService) GetCurConfigList(bool) []kernel.KeyDescrValue   { return nil }
func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue      { return nil }
func (*kernelService) GetStatistics() []kernel.KeyValue               { return nil }
func (*kernelService) Freeze()                                        {}
func (*kernelService) Start(*myKernel) error                          { return nil }
func (*kernelService) SwitchNextToCur()                               {}
func (*kernelService) IsStarted() bool                                { return true }
func (*kernelService) Stop(*myKernel)                                 {}

Changes to kernel/impl/web.go.

43
44
45
46
47
48
49

50
51
52
53
54

55
56
57
58
59
60
61
func (ws *webService) Initialize(logger *logger.Logger) {
	ws.logger = logger
	ws.descr = descriptionMap{
		kernel.WebAssetDir: {
			"Asset file  directory",
			func(val string) (any, error) {
				val = filepath.Clean(val)

				if finfo, err := os.Stat(val); err == nil && finfo.IsDir() {
					return val, nil
				} else {
					return nil, err
				}

			},
			true,
		},
		kernel.WebBaseURL: {
			"Base URL",
			func(val string) (any, error) {
				if _, err := url.Parse(val); err != nil {







>
|

<
<

>







43
44
45
46
47
48
49
50
51
52


53
54
55
56
57
58
59
60
61
func (ws *webService) Initialize(logger *logger.Logger) {
	ws.logger = logger
	ws.descr = descriptionMap{
		kernel.WebAssetDir: {
			"Asset file  directory",
			func(val string) (any, error) {
				val = filepath.Clean(val)
				finfo, err := os.Stat(val)
				if err == nil && finfo.IsDir() {
					return val, nil


				}
				return nil, err
			},
			true,
		},
		kernel.WebBaseURL: {
			"Base URL",
			func(val string) (any, error) {
				if _, err := url.Parse(val); err != nil {
78
79
80
81
82
83
84

85
86
87
88
89
90
91
					return "", err
				}
				return ap.String(), nil
			},
			true},
		kernel.WebMaxRequestSize:   {"Max Request Size", parseInt64, true},
		kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true},

		kernel.WebSecureCookie:     {"Secure cookie", parseBool, true},
		kernel.WebTokenLifetimeAPI: {
			"Token lifetime API",
			makeDurationParser(10*time.Minute, 0, 1*time.Hour),
			true,
		},
		kernel.WebTokenLifetimeHTML: {







>







78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
					return "", err
				}
				return ap.String(), nil
			},
			true},
		kernel.WebMaxRequestSize:   {"Max Request Size", parseInt64, true},
		kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true},
		kernel.WebProfiling:        {"Runtime profiling", parseBool, true},
		kernel.WebSecureCookie:     {"Secure cookie", parseBool, true},
		kernel.WebTokenLifetimeAPI: {
			"Token lifetime API",
			makeDurationParser(10*time.Minute, 0, 1*time.Hour),
			true,
		},
		kernel.WebTokenLifetimeHTML: {
107
108
109
110
111
112
113

114
115
116
117
118
119
120
	ws.next = interfaceMap{
		kernel.WebAssetDir:          "",
		kernel.WebBaseURL:           "http://127.0.0.1:23123/",
		kernel.WebListenAddress:     "127.0.0.1:23123",
		kernel.WebMaxRequestSize:    int64(16 * 1024 * 1024),
		kernel.WebPersistentCookie:  false,
		kernel.WebSecureCookie:      true,

		kernel.WebTokenLifetimeAPI:  1 * time.Hour,
		kernel.WebTokenLifetimeHTML: 10 * time.Minute,
		kernel.WebURLPrefix:         "/",
	}
}

func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc {







>







108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
	ws.next = interfaceMap{
		kernel.WebAssetDir:          "",
		kernel.WebBaseURL:           "http://127.0.0.1:23123/",
		kernel.WebListenAddress:     "127.0.0.1:23123",
		kernel.WebMaxRequestSize:    int64(16 * 1024 * 1024),
		kernel.WebPersistentCookie:  false,
		kernel.WebSecureCookie:      true,
		kernel.WebProfiling:         false,
		kernel.WebTokenLifetimeAPI:  1 * time.Hour,
		kernel.WebTokenLifetimeHTML: 10 * time.Minute,
		kernel.WebURLPrefix:         "/",
	}
}

func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc {
139
140
141
142
143
144
145

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160











161
162
163
164
165
166
167
168

func (ws *webService) Start(kern *myKernel) error {
	baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string)
	listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
	urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string)
	persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool)
	secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool)

	maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64)
	if maxRequestSize < 1024 {
		maxRequestSize = 1024
	}

	if !strings.HasSuffix(baseURL, urlPrefix) {
		ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg(
			"url-prefix is not a suffix of base-url")
		return errWrongBasePrefix
	}

	if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() {
		ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled")
	}












	srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg)
	if err != nil {
		ws.logger.Error().Err(err).Msg("Unable to create")
		return err
	}
	if kern.core.GetNextConfig(kernel.CoreDebug).(bool) {
		srvw.SetDebug()







>















>
>
>
>
>
>
>
>
>
>
>
|







141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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 (ws *webService) Start(kern *myKernel) error {
	baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string)
	listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
	urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string)
	persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool)
	secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool)
	profile := ws.GetNextConfig(kernel.WebProfiling).(bool)
	maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64)
	if maxRequestSize < 1024 {
		maxRequestSize = 1024
	}

	if !strings.HasSuffix(baseURL, urlPrefix) {
		ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg(
			"url-prefix is not a suffix of base-url")
		return errWrongBasePrefix
	}

	if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() {
		ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled")
	}

	sd := impl.ServerData{
		Log:              ws.logger,
		ListenAddr:       listenAddr,
		BaseURL:          baseURL,
		URLPrefix:        urlPrefix,
		MaxRequestSize:   maxRequestSize,
		Auth:             kern.auth.manager,
		PersistentCookie: persistentCookie,
		SecureCookie:     secureCookie,
		Profiling:        profile,
	}
	srvw := impl.New(sd)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg)
	if err != nil {
		ws.logger.Error().Err(err).Msg("Unable to create")
		return err
	}
	if kern.core.GetNextConfig(kernel.CoreDebug).(bool) {
		srvw.SetDebug()

Changes to kernel/kernel.go.

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
	// StartService start the given service.
	StartService(Service) error

	// RestartService stops and restarts the given service, while maintaining service dependencies.
	RestartService(Service) error

	// StopService stop the given service.
	StopService(Service) error

	// GetServiceStatistics returns a key/value list with statistical data.
	GetServiceStatistics(Service) []KeyValue

	// DumpIndex writes some data about the internal index into a writer.
	DumpIndex(io.Writer)








|







90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
	// StartService start the given service.
	StartService(Service) error

	// RestartService stops and restarts the given service, while maintaining service dependencies.
	RestartService(Service) error

	// StopService stop the given service.
	StopService(Service)

	// GetServiceStatistics returns a key/value list with statistical data.
	GetServiceStatistics(Service) []KeyValue

	// DumpIndex writes some data about the internal index into a writer.
	DumpIndex(io.Writer)

189
190
191
192
193
194
195

196
197
198
199
200
201
202

// Constants for web service keys.
const (
	WebAssetDir          = "asset-dir"
	WebBaseURL           = "base-url"
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"

	WebMaxRequestSize    = "max-request-size"
	WebSecureCookie      = "secure"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)








>







189
190
191
192
193
194
195
196
197
198
199
200
201
202
203

// Constants for web service keys.
const (
	WebAssetDir          = "asset-dir"
	WebBaseURL           = "base-url"
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"
	WebProfiling         = "profiling"
	WebMaxRequestSize    = "max-request-size"
	WebSecureCookie      = "secure"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)

Changes to logger/message.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"context"
	"net/http"
	"strconv"
	"sync"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
)

// Message presents a message to log.
type Message struct {
	logger *Logger
	level  Level







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"context"
	"net/http"
	"strconv"
	"sync"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
)

// Message presents a message to log.
type Message struct {
	logger *Logger
	level  Level

Changes to parser/blob/blob.go.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package blob provides a parser of binary data.
package blob

import (
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{







|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package blob provides a parser of binary data.
package blob

import (
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{

Changes to parser/draw/canvas.go.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

package draw

import (
	"bytes"
	"fmt"
	"image"
	"sort"
	"unicode/utf8"
)

// newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative
// value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas
// can fail if the diagram contains invalid UTF-8 sequences.
func newCanvas(data []byte) (*canvas, error) {







|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

package draw

import (
	"bytes"
	"fmt"
	"image"
	"slices"
	"unicode/utf8"
)

// newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative
// value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas
// can fail if the diagram contains invalid UTF-8 sequences.
func newCanvas(data []byte) (*canvas, error) {
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// size returns the visual dimensions of the Canvas.
func (c *canvas) size() image.Point { return c.siz }

// findObjects finds all objects (lines, polygons, and text) within the underlying grid.
func (c *canvas) findObjects() {
	c.findPaths()
	c.findTexts()
	sort.Sort(c.objs)
}

// findPaths by starting with a point that wasn't yet visited, beginning at the top
// left of the grid.
func (c *canvas) findPaths() {
	for y := range c.siz.Y {
		p := point{y: y}







|







90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// size returns the visual dimensions of the Canvas.
func (c *canvas) size() image.Point { return c.siz }

// findObjects finds all objects (lines, polygons, and text) within the underlying grid.
func (c *canvas) findObjects() {
	c.findPaths()
	c.findTexts()
	slices.SortFunc(c.objs, compare)
}

// findPaths by starting with a point that wasn't yet visited, beginning at the top
// left of the grid.
func (c *canvas) findPaths() {
	for y := range c.siz.Y {
		p := point{y: y}

Changes to parser/draw/draw.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// It is not a parser registered by the general parser framework (directed by
// metadata "syntax" of a zettel). It will be used when a zettel is evaluated.
package draw

import (
	"strconv"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{







|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// It is not a parser registered by the general parser framework (directed by
// metadata "syntax" of a zettel). It will be used when a zettel is evaluated.
package draw

import (
	"strconv"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
124
125
126
127
128
129
130
131
132
133
134
135
136
			return n
		}
	}
	return defVal
}

func canvasErrMsg(err error) ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("Error:", err.Error())
}

func noSVGErrMsg() ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("NO", "IMAGE")
}







|



|

124
125
126
127
128
129
130
131
132
133
134
135
136
			return n
		}
	}
	return defVal
}

func canvasErrMsg(err error) ast.InlineSlice {
	return ast.InlineSlice{&ast.TextNode{Text: "Error: " + err.Error()}}
}

func noSVGErrMsg() ast.InlineSlice {
	return ast.InlineSlice{&ast.TextNode{Text: "NO IMAGE"}}
}

Changes to parser/draw/draw_test.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package draw_test

import (
	"testing"

	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package draw_test

import (
	"testing"

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {

Changes to parser/draw/object.go.

18
19
20
21
22
23
24
25



26
27
28
29
30
31
32
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import "fmt"




// object represents one of an open path, a closed path, or text.
type object struct {
	// points always starts with the top most, then left most point, proceeding to the right.
	points   []point
	text     []rune
	corners  []point







|
>
>
>







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import (
	"cmp"
	"fmt"
)

// object represents one of an open path, a closed path, or text.
type object struct {
	// points always starts with the top most, then left most point, proceeding to the right.
	points   []point
	text     []rune
	corners  []point
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
		o.text[i] = rune(ch)
	}
}

// objects implements a sortable collection of Object interfaces.
type objects []*object

func (o objects) Len() int      { return len(o) }
func (o objects) Swap(i, j int) { o[i], o[j] = o[j], o[i] }

// Less returns in order top most, then left most.
func (o objects) Less(i, j int) bool {
	// TODO(dhobsd): This doesn't catch every z-index case we could possibly want. We should
	// support z-indexing of objects through an a2s tag.
	l := o[i]
	r := o[j]
	lt := l.isJustText()
	rt := r.isJustText()
	if lt != rt {
		return rt
	}
	lp := l.Points()[0]
	rp := r.Points()[0]
	if lp.y != rp.y {
		return lp.y < rp.y
	}
	return lp.x < rp.x
}

const (
	dirNone = iota // No directionality
	dirH           // Horizontal
	dirV           // Vertical
	dirSE          // South-East







<
<
|
<
<


<
<



|




|

|







103
104
105
106
107
108
109


110


111
112


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
		o.text[i] = rune(ch)
	}
}

// objects implements a sortable collection of Object interfaces.
type objects []*object



func compare(l, r *object) int {


	// TODO(dhobsd): This doesn't catch every z-index case we could possibly want. We should
	// support z-indexing of objects through an a2s tag.


	lt := l.isJustText()
	rt := r.isJustText()
	if lt != rt {
		return 1
	}
	lp := l.Points()[0]
	rp := r.Points()[0]
	if lp.y != rp.y {
		return cmp.Compare(lp.y, rp.y)
	}
	return cmp.Compare(lp.x, rp.x)
}

const (
	dirNone = iota // No directionality
	dirH           // Horizontal
	dirV           // Vertical
	dirSE          // South-East

Changes to parser/markdown/markdown.go.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	"strconv"
	"strings"

	gm "github.com/yuin/goldmark"
	gmAst "github.com/yuin/goldmark/ast"
	gmText "github.com/yuin/goldmark/text"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {







|
|







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	"strconv"
	"strings"

	gm "github.com/yuin/goldmark"
	gmAst "github.com/yuin/goldmark/ast"
	gmText "github.com/yuin/goldmark/text"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
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
		return p.acceptRawHTML(n)
	}
	panic(fmt.Sprintf("Unhandled inline node %v", node.Kind()))
}

func (p *mdP) acceptText(node *gmAst.Text) ast.InlineSlice {
	segment := node.Segment




	if node.IsRaw() {
		return splitText(string(segment.Value(p.source)))
	}
	ins := splitText(string(segment.Value(p.source)))
	result := make(ast.InlineSlice, 0, len(ins)+1)
	for _, in := range ins {
		if tn, ok := in.(*ast.TextNode); ok {
			tn.Text = cleanText([]byte(tn.Text), true)
		}
		result = append(result, in)
	}
	if node.HardLineBreak() {
		result = append(result, &ast.BreakNode{Hard: true})
	} else if node.SoftLineBreak() {
		result = append(result, &ast.BreakNode{Hard: false})
	}
	return result
}

// splitText transform the text into a sequence of TextNode and SpaceNode
func splitText(text string) ast.InlineSlice {
	if text == "" {
		return nil
	}
	result := make(ast.InlineSlice, 0, 1)

	state := 0 // 0=unknown,1=non-spaces,2=spaces
	lastPos := 0
	for pos, ch := range text {
		if input.IsSpace(ch) {
			if state == 1 {
				result = append(result, &ast.TextNode{Text: text[lastPos:pos]})
				lastPos = pos
			}
			state = 2
		} else {
			if state == 2 {
				result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:pos]})
				lastPos = pos
			}
			state = 1
		}
	}
	switch state {
	case 1:
		result = append(result, &ast.TextNode{Text: text[lastPos:]})
	case 2:
		result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:]})
	default:
		panic(fmt.Sprintf("Unexpected state %v", state))
	}
	return result
}

var ignoreAfterBS = map[byte]struct{}{
	'!': {}, '"': {}, '#': {}, '$': {}, '%': {}, '&': {}, '\'': {}, '(': {},
	')': {}, '*': {}, '+': {}, ',': {}, '-': {}, '.': {}, '/': {}, ':': {},
	';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {},







>
>
>
>

|

<
|
<
|
<
<
|
<





<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







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
		return p.acceptRawHTML(n)
	}
	panic(fmt.Sprintf("Unhandled inline node %v", node.Kind()))
}

func (p *mdP) acceptText(node *gmAst.Text) ast.InlineSlice {
	segment := node.Segment
	text := segment.Value(p.source)
	if text == nil {
		return nil
	}
	if node.IsRaw() {
		return ast.InlineSlice{&ast.TextNode{Text: string(text)}}
	}

	result := make(ast.InlineSlice, 0, 2)

	in := &ast.TextNode{Text: cleanText(text, true)}


	result = append(result, in)

	if node.HardLineBreak() {
		result = append(result, &ast.BreakNode{Hard: true})
	} else if node.SoftLineBreak() {
		result = append(result, &ast.BreakNode{Hard: false})
	}



































	return result
}

var ignoreAfterBS = map[byte]struct{}{
	'!': {}, '"': {}, '#': {}, '$': {}, '%': {}, '&': {}, '\'': {}, '(': {},
	')': {}, '*': {}, '+': {}, ',': {}, '-': {}, '.': {}, '/': {}, ':': {},
	';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {},
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
	if lastPos < len(text) {
		sb.Write(text[lastPos:])
	}
	return sb.String()
}

func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice {


























	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:    ast.LiteralProg,
			Attrs:   nil, //TODO
			Content: cleanCodeSpan(node.Text(p.source)),
		},
	}
}

func cleanCodeSpan(text []byte) []byte {
	if len(text) == 0 {
		return nil
	}
	lastPos := 0
	var buf bytes.Buffer
	for pos, ch := range text {
		if ch == '\n' {
			buf.Write(text[lastPos:pos])
			if pos < len(text)-1 {
				buf.WriteByte(' ')
			}
			lastPos = pos + 1
		}
	}
	buf.Write(text[lastPos:])
	return buf.Bytes()
}

func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice {
	kind := ast.FormatEmph
	if node.Level == 2 {
		kind = ast.FormatStrong
	}
	return ast.InlineSlice{
		&ast.FormatNode{







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|




<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







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
	if lastPos < len(text) {
		sb.Write(text[lastPos:])
	}
	return sb.String()
}

func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice {
	var segBuf bytes.Buffer
	for c := node.FirstChild(); c != nil; c = c.NextSibling() {
		segment := c.(*gmAst.Text).Segment
		segBuf.Write(segment.Value(p.source))
	}
	content := segBuf.Bytes()

	// Clean code span
	if len(content) == 0 {
		content = nil
	} else {
		lastPos := 0
		var buf bytes.Buffer
		for pos, ch := range content {
			if ch == '\n' {
				buf.Write(content[lastPos:pos])
				if pos < len(content)-1 {
					buf.WriteByte(' ')
				}
				lastPos = pos + 1
			}
		}
		buf.Write(content[lastPos:])
		content = buf.Bytes()
	}

	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:    ast.LiteralProg,
			Attrs:   nil, //TODO
			Content: content,
		},
	}
}




















func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice {
	kind := ast.FormatEmph
	if node.Level == 2 {
		kind = ast.FormatStrong
	}
	return ast.InlineSlice{
		&ast.FormatNode{

Deleted parser/markdown/markdown_test.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package markdown

import (
	"strings"
	"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
		for _, in := range splitText(tc.text) {
			switch n := in.(type) {
			case *ast.TextNode:
				sb.WriteByte('T')
				sb.WriteString(n.Text)
			case *ast.SpaceNode:
				sb.WriteByte('S')
				sb.WriteString(n.Lexeme)
			default:
				sb.WriteByte('Q')
			}
		}
		got := sb.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.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package none provides a none-parser, e.g. for zettel with just metadata.
package none

import (
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{







|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package none provides a none-parser, e.g. for zettel with just metadata.
package none

import (
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{

Changes to parser/parser.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package parser

import (
	"context"
	"fmt"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)








|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package parser

import (
	"context"
	"fmt"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
func ParseMetadata(value string) ast.InlineSlice {
	return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk)
}

// ParseSpacedText returns an inline slice that consists just of test and space node.
// No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice.
func ParseSpacedText(s string) ast.InlineSlice {
	return ast.CreateInlineSliceFromWords(meta.ListFromValue(s)...)
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") }

// ParseDescription returns a suitable description stored in the metadata as an inline slice.
// This is done for an image in most cases.
func ParseDescription(m *meta.Meta) ast.InlineSlice {
	if m == nil {
		return nil
	}
	if descr, found := m.Get(api.KeySummary); found {
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return ast.CreateInlineSliceFromWords("Zettel", "without", "title:", m.Zid.String())
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {







|



















|







115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
func ParseMetadata(value string) ast.InlineSlice {
	return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk)
}

// ParseSpacedText returns an inline slice that consists just of test and space node.
// No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice.
func ParseSpacedText(s string) ast.InlineSlice {
	return ast.InlineSlice{&ast.TextNode{Text: strings.Join(meta.ListFromValue(s), " ")}}
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") }

// ParseDescription returns a suitable description stored in the metadata as an inline slice.
// This is done for an image in most cases.
func ParseDescription(m *meta.Meta) ast.InlineSlice {
	if m == nil {
		return nil
	}
	if descr, found := m.Get(api.KeySummary); found {
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title: " + m.Zid.String()}}
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {

Changes to parser/plain/plain.go.

12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
//-----------------------------------------------------------------------------

// Package plain provides a parser for plain text data.
package plain

import (
	"bytes"
	"strings"


	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{







<

>
|
|
<







12
13
14
15
16
17
18

19
20
21
22

23
24
25
26
27
28
29
//-----------------------------------------------------------------------------

// Package plain provides a parser for plain text data.
package plain

import (
	"bytes"


	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
121
122
123
124
125
126
127
128
129
130
131
132
133
134


135


136
137
138
139
140
141
142
143
	return ast.InlineSlice{&ast.EmbedBLOBNode{
		Blob:   []byte(svgSrc),
		Syntax: syntax,
	}}
}

func scanSVG(inp *input.Input) string {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	}
	svgSrc := string(inp.Src[inp.Pos:])
	if !strings.HasPrefix(svgSrc, "<svg ") {
		return ""
	}


	// TODO: check proper end </svg>


	return svgSrc
}

func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
	_, err := rd.ReadAll()
	result := ast.BlockSlice{
		&ast.VerbatimNode{







<
|
<
|
|


>
>
|
>
>
|







120
121
122
123
124
125
126

127

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
	return ast.InlineSlice{&ast.EmbedBLOBNode{
		Blob:   []byte(svgSrc),
		Syntax: syntax,
	}}
}

func scanSVG(inp *input.Input) string {

	inp.SkipSpace()

	pos := inp.Pos
	if !inp.Accept("<svg") {
		return ""
	}
	ch := inp.Ch
	if input.IsSpace(ch) || input.IsEOLEOS(ch) || ch == '>' {
		// TODO: check proper end </svg>
		return string(inp.Src[pos:])
	}
	return ""
}

func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
	_, err := rd.ReadAll()
	result := ast.BlockSlice{
		&ast.VerbatimNode{

Added parser/plain/plain_test.go.



































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package plain_test

import (
	"testing"

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func TestParseSVG(t *testing.T) {
	testCases := []struct {
		name string
		src  string
		exp  string
	}{
		{"common", " <svg bla", "(INLINE (EMBED-BLOB () \"svg\" \"<svg bla\"))"},
		{"inkscape", "<svg\nbla", "(INLINE (EMBED-BLOB () \"svg\" \"<svg\\nbla\"))"},
		{"selfmade", "<svg>", "(INLINE (EMBED-BLOB () \"svg\" \"<svg>\"))"},
		{"error", "<svgbla", "(INLINE)"},
		{"error-", "<svg-bla", "(INLINE)"},
		{"error#", "<svg2bla", "(INLINE)"},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			inp := input.NewInput([]byte(tc.src))
			is := parser.ParseInlines(inp, meta.SyntaxSVG)
			trans := szenc.NewTransformer()
			lst := trans.GetSz(&is)
			if got := lst.String(); tc.exp != got {
				t.Errorf("\nexp: %q\ngot: %q", tc.exp, got)
			}
		})
	}
}

Changes to parser/zettelmark/block.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package zettelmark

import (
	"fmt"

	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
)

// parseBlockSlice parses a sequence of blocks.
func (cp *zmkP) parseBlockSlice() ast.BlockSlice {
	inp := cp.inp
	var lastPara *ast.ParaNode







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package zettelmark

import (
	"fmt"

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
)

// parseBlockSlice parses a sequence of blocks.
func (cp *zmkP) parseBlockSlice() ast.BlockSlice {
	inp := cp.inp
	var lastPara *ast.ParaNode
110
111
112
113
114
115
116
117
118






119





120
121
122
123
124
125
126
}

func startsWithSpaceSoftBreak(pn *ast.ParaNode) bool {
	ins := pn.Inlines
	if len(ins) < 2 {
		return false
	}
	_, isSpace := ins[0].(*ast.SpaceNode)
	_, isBreak := ins[1].(*ast.BreakNode)






	return isSpace && isBreak





}

func (cp *zmkP) cleanupListsAfterEOL() {
	for _, l := range cp.lists {
		if lits := len(l.Items); lits > 0 {
			l.Items[lits-1] = append(l.Items[lits-1], &nullItemNode{})
		}







<

>
>
>
>
>
>
|
>
>
>
>
>







110
111
112
113
114
115
116

117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
}

func startsWithSpaceSoftBreak(pn *ast.ParaNode) bool {
	ins := pn.Inlines
	if len(ins) < 2 {
		return false
	}

	_, isBreak := ins[1].(*ast.BreakNode)
	return isBreak && isSpaceText(ins[0])
}
func isSpaceText(node ast.InlineNode) bool {
	if tn, isText := node.(*ast.TextNode); isText {
		for _, ch := range tn.Text {
			if !input.IsSpace(ch) {
				return false
			}
		}
		return true
	}
	return false
}

func (cp *zmkP) cleanupListsAfterEOL() {
	for _, l := range cp.lists {
		if lits := len(l.Items); lits > 0 {
			l.Items[lits-1] = append(l.Items[lits-1], &nullItemNode{})
		}
274
275
276
277
278
279
280

281
282
283
284
285
286
287
288
289
290
291
		if !cont {
			lastPara, _ = bn.(*ast.ParaNode)
		}
	}
}

func (cp *zmkP) parseRegionLastLine(rn *ast.RegionNode) {

	cp.clearStacked() // remove any lists defined in the region
	cp.skipSpace()
	for {
		switch cp.inp.Ch {
		case input.EOS, '\n', '\r':
			return
		}
		in := cp.parseInline()
		if in == nil {
			return
		}







>

|

|







284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
		if !cont {
			lastPara, _ = bn.(*ast.ParaNode)
		}
	}
}

func (cp *zmkP) parseRegionLastLine(rn *ast.RegionNode) {
	inp := cp.inp
	cp.clearStacked() // remove any lists defined in the region
	inp.SkipSpace()
	for {
		switch inp.Ch {
		case input.EOS, '\n', '\r':
			return
		}
		in := cp.parseInline()
		if in == nil {
			return
		}
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
	if delims < 3 {
		return nil, false
	}
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	if delims > 7 {
		delims = 7
	}
	hn = &ast.HeadingNode{Level: delims - 2, Inlines: nil}
	for {
		if input.IsEOLEOS(inp.Ch) {
			return hn, true







|







311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
	if delims < 3 {
		return nil, false
	}
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	if delims > 7 {
		delims = 7
	}
	hn = &ast.HeadingNode{Level: delims - 2, Inlines: nil}
	for {
		if input.IsEOLEOS(inp.Ch) {
			return hn, true
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// parseNestedList parses a list.
func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) {
	inp := cp.inp
	kinds := cp.parseNestedListKinds()
	if kinds == nil {
		return nil, false
	}
	cp.skipSpace()
	if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) {
		return nil, false
	}

	if len(kinds) < len(cp.lists) {
		cp.lists = cp.lists[:len(kinds)]
	}







|







358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// parseNestedList parses a list.
func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) {
	inp := cp.inp
	kinds := cp.parseNestedListKinds()
	if kinds == nil {
		return nil, false
	}
	inp.SkipSpace()
	if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) {
		return nil, false
	}

	if len(kinds) < len(cp.lists) {
		cp.lists = cp.lists[:len(kinds)]
	}
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	descrl := cp.descrl
	if descrl == nil {
		descrl = &ast.DescriptionListNode{}
		cp.descrl = descrl
	}
	descrl.Descriptions = append(descrl.Descriptions, ast.Description{})
	defPos := len(descrl.Descriptions) - 1







|







443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	descrl := cp.descrl
	if descrl == nil {
		descrl = &ast.DescriptionListNode{}
		cp.descrl = descrl
	}
	descrl.Descriptions = append(descrl.Descriptions, ast.Description{})
	defPos := len(descrl.Descriptions) - 1
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
func (cp *zmkP) parseDefDescr() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	descrl := cp.descrl
	if descrl == nil || len(descrl.Descriptions) == 0 {
		return nil, false
	}
	defPos := len(descrl.Descriptions) - 1
	if len(descrl.Descriptions[defPos].Term) == 0 {
		return nil, false







|







477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
func (cp *zmkP) parseDefDescr() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	descrl := cp.descrl
	if descrl == nil || len(descrl.Descriptions) == 0 {
		return nil, false
	}
	defPos := len(descrl.Descriptions) - 1
	if len(descrl.Descriptions[defPos].Term) == 0 {
		return nil, false

Changes to parser/zettelmark/inline.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package zettelmark

import (
	"bytes"
	"fmt"
	"strings"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/zettel/meta"
)

// parseInlineSlice parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) {
	inp := cp.inp







|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package zettelmark

import (
	"bytes"
	"fmt"
	"strings"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/zettel/meta"
)

// parseInlineSlice parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) {
	inp := cp.inp
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
		var in ast.InlineNode
		success := false
		switch inp.Ch {
		case input.EOS:
			return nil
		case '\n', '\r':
			return cp.parseSoftBreak()
		case ' ', '\t':
			return cp.parseSpace()
		case '[':
			inp.Next()
			switch inp.Ch {
			case '[':
				in, success = cp.parseLink()
			case '@':
				in, success = cp.parseCite()







<
<







47
48
49
50
51
52
53


54
55
56
57
58
59
60
		var in ast.InlineNode
		success := false
		switch inp.Ch {
		case input.EOS:
			return nil
		case '\n', '\r':
			return cp.parseSoftBreak()


		case '[':
			inp.Next()
			switch inp.Ch {
			case '[':
				in, success = cp.parseLink()
			case '@':
				in, success = cp.parseCite()
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
		return cp.parseBackslashRest()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp







|







99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
		return cp.parseBackslashRest()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
		return &ast.TextNode{Text: "\u00a0"}
	}
	pos := inp.Pos
	inp.Next()
	return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
}

func (cp *zmkP) parseSpace() *ast.SpaceNode {
	inp := cp.inp
	pos := inp.Pos
	for {
		inp.Next()
		switch inp.Ch {
		case ' ', '\t':
		default:
			return &ast.SpaceNode{Lexeme: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseSoftBreak() *ast.BreakNode {
	cp.inp.EatEOL()
	return &ast.BreakNode{}
}

func (cp *zmkP) parseLink() (*ast.LinkNode, bool) {
	if ref, is, ok := cp.parseReference('[', ']'); ok {







<
<
<
<
<
<
<
<
<
<
<
<
<







131
132
133
134
135
136
137













138
139
140
141
142
143
144
		return &ast.TextNode{Text: "\u00a0"}
	}
	pos := inp.Pos
	inp.Next()
	return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
}














func (cp *zmkP) parseSoftBreak() *ast.BreakNode {
	cp.inp.EatEOL()
	return &ast.BreakNode{}
}

func (cp *zmkP) parseLink() (*ast.LinkNode, bool) {
	if ref, is, ok := cp.parseReference('[', ']'); ok {
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
func hasQueryPrefix(src []byte) bool {
	return len(src) > len(ast.QueryPrefix) && string(src[:len(ast.QueryPrefix)]) == ast.QueryPrefix
}

func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, is ast.InlineSlice, _ bool) {
	inp := cp.inp
	inp.Next()
	cp.skipSpace()
	if inp.Ch == openCh {
		// Additional opening chars result in a fail
		return "", nil, false
	}
	pos := inp.Pos
	if !hasQueryPrefix(inp.Src[pos:]) {
		hasSpace, ok := cp.readReferenceToSep(closeCh)







|







157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
func hasQueryPrefix(src []byte) bool {
	return len(src) > len(ast.QueryPrefix) && string(src[:len(ast.QueryPrefix)]) == ast.QueryPrefix
}

func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, is ast.InlineSlice, _ bool) {
	inp := cp.inp
	inp.Next()
	inp.SkipSpace()
	if inp.Ch == openCh {
		// Additional opening chars result in a fail
		return "", nil, false
	}
	pos := inp.Pos
	if !hasQueryPrefix(inp.Src[pos:]) {
		hasSpace, ok := cp.readReferenceToSep(closeCh)
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
			if hasSpace {
				return "", nil, false
			}
			inp.SetPos(pos)
		}
	}

	cp.skipSpace()
	pos = inp.Pos
	if !cp.readReferenceToClose(closeCh) {
		return "", nil, false
	}
	ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	inp.Next()
	if inp.Ch != closeCh {







|







190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
			if hasSpace {
				return "", nil, false
			}
			inp.SetPos(pos)
		}
	}

	inp.SkipSpace()
	pos = inp.Pos
	if !cp.readReferenceToClose(closeCh) {
		return "", nil, false
	}
	ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	inp.Next()
	if inp.Ch != closeCh {
324
325
326
327
328
329
330

331
332
333
334
335
336
337
338
339
340
		return nil, false
	}
	attrs := cp.parseInlineAttributes()
	return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true
}

func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) {

	cp.skipSpace()
	ins := ast.InlineSlice{}
	inp := cp.inp
	for inp.Ch != ']' {
		in := cp.parseInline()
		if in == nil {
			return nil, false
		}
		ins = append(ins, in)
		if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {







>
|

<







309
310
311
312
313
314
315
316
317
318

319
320
321
322
323
324
325
		return nil, false
	}
	attrs := cp.parseInlineAttributes()
	return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true
}

func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) {
	inp := cp.inp
	inp.SkipSpace()
	ins := ast.InlineSlice{}

	for inp.Ch != ']' {
		in := cp.parseInline()
		if in == nil {
			return nil, false
		}
		ins = append(ins, in)
		if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
	if inp.Ch != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	attrs := cp.parseInlineAttributes()
	cp.skipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return &ast.LiteralNode{
				Kind:    ast.LiteralComment,
				Attrs:   attrs,
				Content: append([]byte(nil), inp.Src[pos:inp.Pos]...),







|







380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
	if inp.Ch != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	attrs := cp.parseInlineAttributes()
	inp.SkipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return &ast.LiteralNode{
				Kind:    ast.LiteralComment,
				Attrs:   attrs,
				Content: append([]byte(nil), inp.Src[pos:inp.Pos]...),

Changes to parser/zettelmark/post-processor.go.

72
73
74
75
76
77
78

79
80
81
82
83
84
85
86
87
88
89

func (pp *postProcessor) visitRegion(rn *ast.RegionNode) {
	oldVerse := pp.inVerse
	if rn.Kind == ast.RegionVerse {
		pp.inVerse = true
	}
	pp.visitBlockSlice(&rn.Blocks)

	if len(rn.Inlines) > 0 {
		pp.visitInlineSlice(&rn.Inlines)
	}
	pp.inVerse = oldVerse
}

func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}
	if ln.Kind != ast.NestedListQuote {







>



<







72
73
74
75
76
77
78
79
80
81
82

83
84
85
86
87
88
89

func (pp *postProcessor) visitRegion(rn *ast.RegionNode) {
	oldVerse := pp.inVerse
	if rn.Kind == ast.RegionVerse {
		pp.inVerse = true
	}
	pp.visitBlockSlice(&rn.Blocks)
	pp.inVerse = oldVerse
	if len(rn.Inlines) > 0 {
		pp.visitInlineSlice(&rn.Inlines)
	}

}

func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}
	if ln.Kind != ast.NestedListQuote {
364
365
366
367
368
369
370
371
372
373
374
375
376





377
378
379
380
381
382
383
}

// processInlineSliceHead removes leading spaces and empty text.
func (pp *postProcessor) processInlineSliceHead(is *ast.InlineSlice) {
	ins := *is
	for i, in := range ins {
		switch in := in.(type) {
		case *ast.SpaceNode:
			if pp.inVerse {
				*is = ins[i:]
				return
			}
		case *ast.TextNode:





			if len(in.Text) > 0 {
				*is = ins[i:]
				return
			}
		default:
			*is = ins[i:]
			return







|
|



|
>
>
>
>
>







364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
}

// processInlineSliceHead removes leading spaces and empty text.
func (pp *postProcessor) processInlineSliceHead(is *ast.InlineSlice) {
	ins := *is
	for i, in := range ins {
		switch in := in.(type) {
		case *ast.TextNode:
			if pp.inVerse && len(in.Text) > 0 {
				*is = ins[i:]
				return
			}
			for len(in.Text) > 0 {
				if ch := in.Text[0]; ch != ' ' && ch != '\t' {
					break
				}
				in.Text = in.Text[1:]
			}
			if len(in.Text) > 0 {
				*is = ins[i:]
				return
			}
		default:
			*is = ins[i:]
			return
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










	ins := *is
	fromPos, toPos := 0, 0
	for fromPos < maxPos {
		ins[toPos] = ins[fromPos]
		fromPos++
		switch in := ins[toPos].(type) {
		case *ast.TextNode:




			fromPos = processTextNode(ins, maxPos, in, fromPos)









		case *ast.SpaceNode:








			if pp.inVerse {
				in.Lexeme = strings.Repeat("\u00a0", in.Count())
			}
			fromPos = processSpaceNode(ins, maxPos, in, toPos, fromPos)
		case *ast.BreakNode:
			if pp.inVerse {
				in.Hard = true
			}
		}
		toPos++
	}
	return toPos
}

func processTextNode(ins ast.InlineSlice, maxPos int, in *ast.TextNode, fromPos int) int {
	for fromPos < maxPos {
		if tn, ok := ins[fromPos].(*ast.TextNode); ok {
			in.Text = in.Text + tn.Text
			fromPos++
		} else {
			break
		}
	}
	return fromPos
}

func processSpaceNode(ins ast.InlineSlice, maxPos int, in *ast.SpaceNode, toPos, fromPos int) int {
	if fromPos < maxPos {
		switch nn := ins[fromPos].(type) {
		case *ast.BreakNode:
			if in.Count() > 1 {
				nn.Hard = true
				ins[toPos] = nn
				fromPos++
			}
		case *ast.LiteralNode:
			if nn.Kind == ast.LiteralComment {
				ins[toPos] = ins[fromPos]
				fromPos++
			}
		}
	}
	return fromPos
}

// processInlineSliceTail removes empty text nodes, breaks and spaces at the end.
func (*postProcessor) processInlineSliceTail(is *ast.InlineSlice, toPos int) int {
	ins := *is
	for toPos > 0 {
		switch n := ins[toPos-1].(type) {
		case *ast.TextNode:

			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
}

















>
>
>
>
|
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>

|

<










<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






>




<








>
>
>
>
>
>
>
>
>
>
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443

444
445
446
447
448
449
450
451
452
453































454
455
456
457
458
459
460
461
462
463
464

465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
	ins := *is
	fromPos, toPos := 0, 0
	for fromPos < maxPos {
		ins[toPos] = ins[fromPos]
		fromPos++
		switch in := ins[toPos].(type) {
		case *ast.TextNode:
			// Merge following TextNodes
			for fromPos < maxPos {
				if tn, ok := ins[fromPos].(*ast.TextNode); ok {
					in.Text = in.Text + tn.Text
					fromPos++
				} else {
					break
				}
			}
			if in.Text == "" {
				continue
			}
			if ch := in.Text[len(in.Text)-1]; ch == ' ' && fromPos < maxPos {
				switch nn := ins[fromPos].(type) {
				case *ast.BreakNode:
					nn.Hard = true
					in.Text = removeTrailingSpaces(in.Text)
				case *ast.LiteralNode:
					if nn.Kind == ast.LiteralComment {
						in.Text = removeTrailingSpaces(in.Text)
					}
				}
			}
			if pp.inVerse {
				in.Text = strings.ReplaceAll(in.Text, " ", "\u00a0")
			}

		case *ast.BreakNode:
			if pp.inVerse {
				in.Hard = true
			}
		}
		toPos++
	}
	return toPos
}
































// processInlineSliceTail removes empty text nodes, breaks and spaces at the end.
func (*postProcessor) processInlineSliceTail(is *ast.InlineSlice, toPos int) int {
	ins := *is
	for toPos > 0 {
		switch n := ins[toPos-1].(type) {
		case *ast.TextNode:
			n.Text = removeTrailingSpaces(n.Text)
			if len(n.Text) > 0 {
				return toPos
			}
		case *ast.BreakNode:

		default:
			return toPos
		}
		toPos--
		ins[toPos] = nil // Kill node to enable garbage collection
	}
	return toPos
}

func removeTrailingSpaces(s string) string {
	for len(s) > 0 {
		if ch := s[len(s)-1]; ch != ' ' && ch != '\t' {
			return s
		}
		s = s[0 : len(s)-1]
	}
	return ""
}

Changes to parser/zettelmark/zettelmark.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"strings"
	"unicode"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{







|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"strings"
	"unicode"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
		inp.Next()
	}
	if pos < inp.Pos {
		return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])}
	}

	// No immediate name: skip spaces
	cp.skipSpace()
	return cp.parseInlineAttributes()
}

func (cp *zmkP) parseInlineAttributes() attrs.Attributes {
	inp := cp.inp
	pos := inp.Pos
	if attrs, success := cp.doParseAttributes(); success {







|







152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
		inp.Next()
	}
	if pos < inp.Pos {
		return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])}
	}

	// No immediate name: skip spaces
	inp.SkipSpace()
	return cp.parseInlineAttributes()
}

func (cp *zmkP) parseInlineAttributes() attrs.Attributes {
	inp := cp.inp
	pos := inp.Pos
	if attrs, success := cp.doParseAttributes(); success {
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
			inp.EatEOL()
		default:
			return
		}
	}
}

func (cp *zmkP) skipSpace() {
	for inp := cp.inp; inp.Ch == ' '; {
		inp.Next()
	}
}

func isNameRune(ch rune) bool {
	return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_'
}







<
<
<
<
<
<



236
237
238
239
240
241
242






243
244
245
			inp.EatEOL()
		default:
			return
		}
	}
}







func isNameRune(ch rune) bool {
	return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_'
}

Changes to parser/zettelmark/zettelmark_fuzz_test.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package zettelmark_test

import (
	"testing"

	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package zettelmark_test

import (
	"testing"

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {

Changes to parser/zettelmark/zettelmark_test.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package zettelmark_test

import (
	"fmt"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

type TestCase struct{ source, want string }







|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package zettelmark_test

import (
	"fmt"
	"strings"
	"testing"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

type TestCase struct{ source, want string }
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	})
}

func TestText(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"abcd", "(PARA abcd)"},
		{"ab cd", "(PARA ab SP cd)"},
		{"abcd ", "(PARA abcd)"},
		{" abcd", "(PARA abcd)"},
		{"\\", "(PARA \\)"},
		{"\\\n", ""},
		{"\\\ndef", "(PARA HB def)"},
		{"\\\r", ""},
		{"\\\rdef", "(PARA HB def)"},
		{"\\\r\n", ""},
		{"\\\r\ndef", "(PARA HB def)"},
		{"\\a", "(PARA a)"},
		{"\\aa", "(PARA aa)"},
		{"a\\a", "(PARA aa)"},
		{"\\+", "(PARA +)"},
		{"\\ ", "(PARA \u00a0)"},
		{"http://a, http://b", "(PARA http://a, SP http://b)"},
	})
}

func TestSpace(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{" ", ""},







|














|







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	})
}

func TestText(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"abcd", "(PARA abcd)"},
		{"ab cd", "(PARA ab cd)"},
		{"abcd ", "(PARA abcd)"},
		{" abcd", "(PARA abcd)"},
		{"\\", "(PARA \\)"},
		{"\\\n", ""},
		{"\\\ndef", "(PARA HB def)"},
		{"\\\r", ""},
		{"\\\rdef", "(PARA HB def)"},
		{"\\\r\n", ""},
		{"\\\r\ndef", "(PARA HB def)"},
		{"\\a", "(PARA a)"},
		{"\\aa", "(PARA aa)"},
		{"a\\a", "(PARA aa)"},
		{"\\+", "(PARA +)"},
		{"\\ ", "(PARA \u00a0)"},
		{"http://a, http://b", "(PARA http://a, http://b)"},
	})
}

func TestSpace(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{" ", ""},
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
		{"[", "(PARA [)"},
		{"[[", "(PARA [[)"},
		{"[[|", "(PARA [[|)"},
		{"[[]", "(PARA [[])"},
		{"[[|]", "(PARA [[|])"},
		{"[[]]", "(PARA [[]])"},
		{"[[|]]", "(PARA [[|]])"},
		{"[[ ]]", "(PARA [[ SP ]])"},
		{"[[\n]]", "(PARA [[ SB ]])"},
		{"[[ a]]", "(PARA (LINK a))"},
		{"[[a ]]", "(PARA [[a SP ]])"},
		{"[[a\n]]", "(PARA [[a SB ]])"},
		{"[[a]]", "(PARA (LINK a))"},
		{"[[12345678901234]]", "(PARA (LINK 12345678901234))"},
		{"[[a]", "(PARA [[a])"},
		{"[[|a]]", "(PARA [[|a]])"},
		{"[[b|]]", "(PARA [[b|]])"},
		{"[[b|a]]", "(PARA (LINK a b))"},
		{"[[b| a]]", "(PARA (LINK a b))"},
		{"[[b%c|a]]", "(PARA (LINK a b%c))"},
		{"[[b%%c|a]]", "(PARA [[b {% c|a]]})"},
		{"[[b|a]", "(PARA [[b|a])"},
		{"[[b\nc|a]]", "(PARA (LINK a b SB c))"},
		{"[[b c|a#n]]", "(PARA (LINK a#n b SP c))"},
		{"[[a]]go", "(PARA (LINK a) go)"},
		{"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"},
		{"[[[[a]]|b]]", "(PARA [[ (LINK a) |b]])"},
		{"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"},
		{"[[[b]c|d]]", "(PARA [ (LINK d b]c))"},
		{"[[a[]c|d]]", "(PARA (LINK d a[]c))"},
		{"[[a[b]|d]]", "(PARA (LINK d a[b]))"},







|


|












|







128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
		{"[", "(PARA [)"},
		{"[[", "(PARA [[)"},
		{"[[|", "(PARA [[|)"},
		{"[[]", "(PARA [[])"},
		{"[[|]", "(PARA [[|])"},
		{"[[]]", "(PARA [[]])"},
		{"[[|]]", "(PARA [[|]])"},
		{"[[ ]]", "(PARA [[ ]])"},
		{"[[\n]]", "(PARA [[ SB ]])"},
		{"[[ a]]", "(PARA (LINK a))"},
		{"[[a ]]", "(PARA [[a ]])"},
		{"[[a\n]]", "(PARA [[a SB ]])"},
		{"[[a]]", "(PARA (LINK a))"},
		{"[[12345678901234]]", "(PARA (LINK 12345678901234))"},
		{"[[a]", "(PARA [[a])"},
		{"[[|a]]", "(PARA [[|a]])"},
		{"[[b|]]", "(PARA [[b|]])"},
		{"[[b|a]]", "(PARA (LINK a b))"},
		{"[[b| a]]", "(PARA (LINK a b))"},
		{"[[b%c|a]]", "(PARA (LINK a b%c))"},
		{"[[b%%c|a]]", "(PARA [[b {% c|a]]})"},
		{"[[b|a]", "(PARA [[b|a])"},
		{"[[b\nc|a]]", "(PARA (LINK a b SB c))"},
		{"[[b c|a#n]]", "(PARA (LINK a#n b c))"},
		{"[[a]]go", "(PARA (LINK a) go)"},
		{"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"},
		{"[[[[a]]|b]]", "(PARA [[ (LINK a) |b]])"},
		{"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"},
		{"[[[b]c|d]]", "(PARA [ (LINK d b]c))"},
		{"[[a[]c|d]]", "(PARA (LINK d a[]c))"},
		{"[[a[b]|d]]", "(PARA (LINK d a[b]))"},
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[@", "(PARA [@)"},
		{"[@]", "(PARA [@])"},
		{"[@a]", "(PARA (CITE a))"},
		{"[@ a]", "(PARA [@ SP a])"},
		{"[@a ]", "(PARA (CITE a))"},
		{"[@a\n]", "(PARA (CITE a))"},
		{"[@a\nx]", "(PARA (CITE a SB x))"},
		{"[@a\n\n]", "(PARA [@a)(PARA ])"},
		{"[@a,\n]", "(PARA (CITE a))"},
		{"[@a,n]", "(PARA (CITE a n))"},
		{"[@a| n]", "(PARA (CITE a n))"},







|







178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[@", "(PARA [@)"},
		{"[@]", "(PARA [@])"},
		{"[@a]", "(PARA (CITE a))"},
		{"[@ a]", "(PARA [@ a])"},
		{"[@a ]", "(PARA (CITE a))"},
		{"[@a\n]", "(PARA (CITE a))"},
		{"[@a\nx]", "(PARA (CITE a SB x))"},
		{"[@a\n\n]", "(PARA [@a)(PARA ])"},
		{"[@a,\n]", "(PARA (CITE a))"},
		{"[@a,n]", "(PARA (CITE a n))"},
		{"[@a| n]", "(PARA (CITE a n))"},
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
		{"{", "(PARA {)"},
		{"{{", "(PARA {{)"},
		{"{{|", "(PARA {{|)"},
		{"{{}", "(PARA {{})"},
		{"{{|}", "(PARA {{|})"},
		{"{{}}", "(PARA {{}})"},
		{"{{|}}", "(PARA {{|}})"},
		{"{{ }}", "(PARA {{ SP }})"},
		{"{{\n}}", "(PARA {{ SB }})"},
		{"{{a }}", "(PARA {{a SP }})"},
		{"{{a\n}}", "(PARA {{a SB }})"},
		{"{{a}}", "(PARA (EMBED a))"},
		{"{{12345678901234}}", "(PARA (EMBED 12345678901234))"},
		{"{{ a}}", "(PARA (EMBED a))"},
		{"{{a}", "(PARA {{a})"},
		{"{{|a}}", "(PARA {{|a}})"},
		{"{{b|}}", "(PARA {{b|}})"},
		{"{{b|a}}", "(PARA (EMBED a b))"},
		{"{{b| a}}", "(PARA (EMBED a b))"},
		{"{{b|a}", "(PARA {{b|a})"},
		{"{{b\nc|a}}", "(PARA (EMBED a b SB c))"},
		{"{{b c|a#n}}", "(PARA (EMBED a#n b SP c))"},
		{"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"},
		{"{{{{a}}|b}}", "(PARA {{ (EMBED a) |b}})"},
		{"{{\\|}}", "(PARA (EMBED %5C%7C))"},
		{"{{\\||a}}", "(PARA (EMBED a |))"},
		{"{{b\\||a}}", "(PARA (EMBED a b|))"},
		{"{{b\\|c|a}}", "(PARA (EMBED a b|c))"},
		{"{{\\}}}", "(PARA (EMBED %5C%7D))"},







|

|











|







216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
		{"{", "(PARA {)"},
		{"{{", "(PARA {{)"},
		{"{{|", "(PARA {{|)"},
		{"{{}", "(PARA {{})"},
		{"{{|}", "(PARA {{|})"},
		{"{{}}", "(PARA {{}})"},
		{"{{|}}", "(PARA {{|}})"},
		{"{{ }}", "(PARA {{ }})"},
		{"{{\n}}", "(PARA {{ SB }})"},
		{"{{a }}", "(PARA {{a }})"},
		{"{{a\n}}", "(PARA {{a SB }})"},
		{"{{a}}", "(PARA (EMBED a))"},
		{"{{12345678901234}}", "(PARA (EMBED 12345678901234))"},
		{"{{ a}}", "(PARA (EMBED a))"},
		{"{{a}", "(PARA {{a})"},
		{"{{|a}}", "(PARA {{|a}})"},
		{"{{b|}}", "(PARA {{b|}})"},
		{"{{b|a}}", "(PARA (EMBED a b))"},
		{"{{b| a}}", "(PARA (EMBED a b))"},
		{"{{b|a}", "(PARA {{b|a})"},
		{"{{b\nc|a}}", "(PARA (EMBED a b SB c))"},
		{"{{b c|a#n}}", "(PARA (EMBED a#n b c))"},
		{"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"},
		{"{{{{a}}|b}}", "(PARA {{ (EMBED a) |b}})"},
		{"{{\\|}}", "(PARA (EMBED %5C%7C))"},
		{"{{\\||a}}", "(PARA (EMBED a |))"},
		{"{{b\\||a}}", "(PARA (EMBED a b|))"},
		{"{{b\\|c|a}}", "(PARA (EMBED a b|c))"},
		{"{{\\}}}", "(PARA (EMBED %5C%7D))"},
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
func TestMark(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[!", "(PARA [!)"},
		{"[!\n", "(PARA [!)"},
		{"[!]", "(PARA (MARK #*))"},
		{"[!][!]", "(PARA (MARK #*) (MARK #*-1))"},
		{"[! ]", "(PARA [! SP ])"},
		{"[!a]", "(PARA (MARK \"a\" #a))"},
		{"[!a][!a]", "(PARA (MARK \"a\" #a) (MARK \"a\" #a-1))"},
		{"[!a ]", "(PARA [!a SP ])"},
		{"[!a_]", "(PARA (MARK \"a_\" #a))"},
		{"[!a_][!a]", "(PARA (MARK \"a_\" #a) (MARK \"a\" #a-1))"},
		{"[!a-b]", "(PARA (MARK \"a-b\" #a-b))"},
		{"[!a|b]", "(PARA (MARK \"a\" #a b))"},
		{"[!a|]", "(PARA (MARK \"a\" #a))"},
		{"[!|b]", "(PARA (MARK #* b))"},
		{"[!|b c]", "(PARA (MARK #* b SP c))"},
	})
}

func TestComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"%", "(PARA %)"},







|


|






|







254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
func TestMark(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[!", "(PARA [!)"},
		{"[!\n", "(PARA [!)"},
		{"[!]", "(PARA (MARK #*))"},
		{"[!][!]", "(PARA (MARK #*) (MARK #*-1))"},
		{"[! ]", "(PARA [! ])"},
		{"[!a]", "(PARA (MARK \"a\" #a))"},
		{"[!a][!a]", "(PARA (MARK \"a\" #a) (MARK \"a\" #a-1))"},
		{"[!a ]", "(PARA [!a ])"},
		{"[!a_]", "(PARA (MARK \"a_\" #a))"},
		{"[!a_][!a]", "(PARA (MARK \"a_\" #a) (MARK \"a\" #a-1))"},
		{"[!a-b]", "(PARA (MARK \"a-b\" #a-b))"},
		{"[!a|b]", "(PARA (MARK \"a\" #a b))"},
		{"[!a|]", "(PARA (MARK \"a\" #a))"},
		{"[!|b]", "(PARA (MARK #* b))"},
		{"[!|b c]", "(PARA (MARK #* b c))"},
	})
}

func TestComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"%", "(PARA %)"},
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
		// Good cases
		{"&lt;", "(PARA <)"},
		{"&#48;", "(PARA 0)"},
		{"&#x4A;", "(PARA J)"},
		{"&#X4a;", "(PARA J)"},
		{"&hellip;", "(PARA \u2026)"},
		{"&nbsp;", "(PARA \u00a0)"},
		{"E: &amp;,&#63;;&#x63;.", "(PARA E: SP &,?;c.)"},
	})
}

func TestVerbatimZettel(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"@@@\n@@@", "(ZETTEL)"},







|







413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
		// Good cases
		{"&lt;", "(PARA <)"},
		{"&#48;", "(PARA 0)"},
		{"&#x4A;", "(PARA J)"},
		{"&#X4a;", "(PARA J)"},
		{"&hellip;", "(PARA \u2026)"},
		{"&nbsp;", "(PARA \u00a0)"},
		{"E: &amp;,&#63;;&#x63;.", "(PARA E: &,?;c.)"},
	})
}

func TestVerbatimZettel(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"@@@\n@@@", "(ZETTEL)"},
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
	}))
}

func TestHeading(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"=h", "(PARA =h)"},
		{"= h", "(PARA = SP h)"},
		{"==h", "(PARA ==h)"},
		{"== h", "(PARA == SP h)"},
		{"===h", "(PARA ===h)"},
		{"=== h", "(H1 h #h)"},
		{"===  h", "(H1 h #h)"},
		{"==== h", "(H2 h #h)"},
		{"===== h", "(H3 h #h)"},
		{"====== h", "(H4 h #h)"},
		{"======= h", "(H5 h #h)"},
		{"======== h", "(H5 h #h)"},
		{"=", "(PARA =)"},
		{"=== h=__=a__", "(H1 h= {_ =a} #h-a)"},
		{"=\n", "(PARA =)"},
		{"a=", "(PARA a=)"},
		{" =", "(PARA =)"},
		{"=== h\na", "(H1 h #h)(PARA a)"},
		{"=== h i {-}", "(H1 h SP i #h-i)[ATTR -]"},
		{"=== h {{a}}", "(H1 h SP (EMBED a) #h)"},
		{"=== h{{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) {







|

|














|
|


|
|







523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
	}))
}

func TestHeading(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"=h", "(PARA =h)"},
		{"= h", "(PARA = h)"},
		{"==h", "(PARA ==h)"},
		{"== h", "(PARA == h)"},
		{"===h", "(PARA ===h)"},
		{"=== h", "(H1 h #h)"},
		{"===  h", "(H1 h #h)"},
		{"==== h", "(H2 h #h)"},
		{"===== h", "(H3 h #h)"},
		{"====== h", "(H4 h #h)"},
		{"======= h", "(H5 h #h)"},
		{"======== h", "(H5 h #h)"},
		{"=", "(PARA =)"},
		{"=== h=__=a__", "(H1 h= {_ =a} #h-a)"},
		{"=\n", "(PARA =)"},
		{"a=", "(PARA a=)"},
		{" =", "(PARA =)"},
		{"=== h\na", "(H1 h #h)(PARA a)"},
		{"=== h i {-}", "(H1 h i #h-i)[ATTR -]"},
		{"=== h {{a}}", "(H1 h  (EMBED a) #h)"},
		{"=== h{{a}}", "(H1 h (EMBED a) #h)"},
		{"=== {{a}}", "(H1 (EMBED a))"},
		{"=== h {{a}}{-}", "(H1 h  (EMBED a)[ATTR -] #h)"},
		{"=== h {{a}} {-}", "(H1 h  (EMBED a) #h)[ATTR -]"},
		{"=== h {-}{{a}}", "(H1 h #h)[ATTR -]"},
		{"=== h{id=abc}", "(H1 h #h)[ATTR id=abc]"},
		{"=== h\n=== h", "(H1 h #h)(H1 h #h-1)"},
	})
}

func TestHRule(t *testing.T) {
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
		{">", "(QL {})"},
	})
}

func TestQuoteList(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"> w1 w2", "(QL {(PARA w1 SP w2)})"},
		{"> w1\n> w2", "(QL {(PARA w1 SB w2)})"},
		{"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"},
	})
}

func TestEnumAfterPara(t *testing.T) {
	t.Parallel()







|







617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
		{">", "(QL {})"},
	})
}

func TestQuoteList(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"> w1 w2", "(QL {(PARA w1 w2)})"},
		{"> w1\n> w2", "(QL {(PARA w1 SB w2)})"},
		{"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"},
	})
}

func TestEnumAfterPara(t *testing.T) {
	t.Parallel()
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
		{"; ", "(PARA ;)"},
		{"; abc", "(DL (DT abc))"},
		{"; abc\ndef", "(DL (DT abc))(PARA def)"},
		{"; abc\n def", "(DL (DT abc))(PARA def)"},
		{"; abc\n  def", "(DL (DT abc SB def))"},
		{":", "(PARA :)"},
		{": ", "(PARA :)"},
		{": abc", "(PARA : SP abc)"},
		{"; abc\n: def", "(DL (DT abc) (DD (PARA def)))"},
		{"; abc\n: def\nghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n  ghi", "(DL (DT abc) (DD (PARA def SB ghi)))"},
		{"; abc\n: def\n\n  ghi", "(DL (DT abc) (DD (PARA def)(PARA ghi)))"},
		{"; abc\n:", "(DL (DT abc))(PARA :)"},
		{"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"},







|







642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
		{"; ", "(PARA ;)"},
		{"; abc", "(DL (DT abc))"},
		{"; abc\ndef", "(DL (DT abc))(PARA def)"},
		{"; abc\n def", "(DL (DT abc))(PARA def)"},
		{"; abc\n  def", "(DL (DT abc SB def))"},
		{":", "(PARA :)"},
		{": ", "(PARA :)"},
		{": abc", "(PARA : abc)"},
		{"; abc\n: def", "(DL (DT abc) (DD (PARA def)))"},
		{"; abc\n: def\nghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n  ghi", "(DL (DT abc) (DD (PARA def SB ghi)))"},
		{"; abc\n: def\n\n  ghi", "(DL (DT abc) (DD (PARA def)(PARA ghi)))"},
		{"; abc\n:", "(DL (DT abc))(PARA :)"},
		{"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"},
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
	})
	checkTcs(t, replace("\"", TestCases{
		{"::a::{py=3}", "(PARA {: a}[ATTR py=3])"},
		{"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"},
		{"::a::{py=$2 3}", "(PARA {: a} {py=$2 SP 3})"},

		{"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"},
	}))
}

func TestTemp(t *testing.T) {







|







746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
	})
	checkTcs(t, replace("\"", TestCases{
		{"::a::{py=3}", "(PARA {: a}[ATTR py=3])"},
		{"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"},
		{"::a::{py=$2 3}", "(PARA {: a} {py=$2 3})"},

		{"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"},
	}))
}

func TestTemp(t *testing.T) {
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
		tv.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		tv.sb.WriteString("(BLOB ")
		tv.sb.WriteString(n.Syntax)
		tv.sb.WriteString(")")
	case *ast.TextNode:
		tv.sb.WriteString(n.Text)
	case *ast.SpaceNode:
		if l := n.Count(); l == 1 {
			tv.sb.WriteString("SP")
		} else {
			fmt.Fprintf(&tv.sb, "SP%d", l)
		}
	case *ast.BreakNode:
		if n.Hard {
			tv.sb.WriteString("HB")
		} else {
			tv.sb.WriteString("SB")
		}
	case *ast.LinkNode:







<
<
<
<
<
<







877
878
879
880
881
882
883






884
885
886
887
888
889
890
		tv.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		tv.sb.WriteString("(BLOB ")
		tv.sb.WriteString(n.Syntax)
		tv.sb.WriteString(")")
	case *ast.TextNode:
		tv.sb.WriteString(n.Text)






	case *ast.BreakNode:
		if n.Hard {
			tv.sb.WriteString("HB")
		} else {
			tv.sb.WriteString("SB")
		}
	case *ast.LinkNode:

Changes to query/compiled.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
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import (
	"math/rand/v2"
	"sort"

	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Compiled is a compiled query, to be used in a Box
type Compiled struct {
	hasQuery bool
	seed     int
	pick     int
	order    []sortOrder
	offset   int // <= 0: no offset
	limit    int // <= 0: no limit

	startMeta []*meta.Meta
	PreMatch  MetaMatchFunc // Precondition for Match and Retrieve
	Terms     []CompiledTerm


}

// MetaMatchFunc is a function determine whethe some metadata should be selected or not.
type MetaMatchFunc func(*meta.Meta) bool

func matchAlways(*meta.Meta) bool { return true }
func matchNever(*meta.Meta) bool  { return false }







|

















>
>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import (
	"math/rand/v2"
	"slices"

	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Compiled is a compiled query, to be used in a Box
type Compiled struct {
	hasQuery bool
	seed     int
	pick     int
	order    []sortOrder
	offset   int // <= 0: no offset
	limit    int // <= 0: no limit

	startMeta []*meta.Meta
	PreMatch  MetaMatchFunc // Precondition for Match and Retrieve
	Terms     []CompiledTerm

	sortFunc sortFunc
}

// MetaMatchFunc is a function determine whethe some metadata should be selected or not.
type MetaMatchFunc func(*meta.Meta) bool

func matchAlways(*meta.Meta) bool { return true }
func matchNever(*meta.Meta) bool  { return false }
69
70
71
72
73
74
75

76
77
78
79






80
81
82
83
84
85
86
87
88
89

90
91
92
93
94
95
96
97

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

118
119
120
121
122
123
124
125
			if term.Match(m) && term.Retrieve(m.Zid) {
				result = append(result, m)
				break
			}
		}
	}
	result = c.pickElements(result)

	result = c.sortElements(result)
	result = c.offsetElements(result)
	return limitElements(result, c.limit)
}







// AfterSearch applies all terms to the metadata list that was searched.
//
// This includes sorting, offset, limit, and picking.
func (c *Compiled) AfterSearch(metaList []*meta.Meta) []*meta.Meta {
	if len(metaList) == 0 {
		return metaList
	}

	if !c.hasQuery {

		return sortMetaByZid(metaList)
	}

	if c.isDeterministic() {
		// We need to sort to make it deterministic
		if len(c.order) == 0 || c.order[0].isRandom() {
			metaList = sortMetaByZid(metaList)
		} else {

			sort.Slice(metaList, createSortFunc(c.order, metaList))
		}
	}
	metaList = c.pickElements(metaList)
	if c.isDeterministic() {
		if len(c.order) > 0 && c.order[0].isRandom() {
			metaList = c.sortRandomly(metaList)
		}
	} else {
		metaList = c.sortElements(metaList)
	}
	metaList = c.offsetElements(metaList)
	return limitElements(metaList, c.limit)
}

func (c *Compiled) sortElements(metaList []*meta.Meta) []*meta.Meta {
	if len(c.order) > 0 {
		if c.order[0].isRandom() {
			metaList = c.sortRandomly(metaList)
		} else {

			sort.Slice(metaList, createSortFunc(c.order, metaList))
		}
	}
	return metaList
}

func (c *Compiled) offsetElements(metaList []*meta.Meta) []*meta.Meta {
	if c.offset == 0 {







>




>
>
>
>
>
>










>
|





|

>
|



















>
|







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
			if term.Match(m) && term.Retrieve(m.Zid) {
				result = append(result, m)
				break
			}
		}
	}
	result = c.pickElements(result)
	c.ensureSortFunc()
	result = c.sortElements(result)
	result = c.offsetElements(result)
	return limitElements(result, c.limit)
}

func (c *Compiled) ensureSortFunc() {
	if c.sortFunc == nil {
		c.sortFunc = buildSortFunc(c.order)
	}
}

// AfterSearch applies all terms to the metadata list that was searched.
//
// This includes sorting, offset, limit, and picking.
func (c *Compiled) AfterSearch(metaList []*meta.Meta) []*meta.Meta {
	if len(metaList) == 0 {
		return metaList
	}

	if !c.hasQuery {
		slices.SortFunc(metaList, defaultMetaSort)
		return metaList
	}

	if c.isDeterministic() {
		// We need to sort to make it deterministic
		if len(c.order) == 0 || c.order[0].isRandom() {
			slices.SortFunc(metaList, defaultMetaSort)
		} else {
			c.ensureSortFunc()
			slices.SortFunc(metaList, c.sortFunc)
		}
	}
	metaList = c.pickElements(metaList)
	if c.isDeterministic() {
		if len(c.order) > 0 && c.order[0].isRandom() {
			metaList = c.sortRandomly(metaList)
		}
	} else {
		metaList = c.sortElements(metaList)
	}
	metaList = c.offsetElements(metaList)
	return limitElements(metaList, c.limit)
}

func (c *Compiled) sortElements(metaList []*meta.Meta) []*meta.Meta {
	if len(c.order) > 0 {
		if c.order[0].isRandom() {
			metaList = c.sortRandomly(metaList)
		} else {
			c.ensureSortFunc()
			slices.SortFunc(metaList, c.sortFunc)
		}
	}
	return metaList
}

func (c *Compiled) offsetElements(metaList []*meta.Meta) []*meta.Meta {
	if c.offset == 0 {
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
	for i := range count {
		last := len(order) - i
		n := rnd.IntN(last)
		picked[i] = order[n]
		order[n] = order[last-1]
	}
	order = nil
	sort.Ints(picked)
	result := make([]*meta.Meta, count)
	for i, p := range picked {
		result[i] = metaList[p]
	}
	return result
}








|







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
	for i := range count {
		last := len(order) - i
		n := rnd.IntN(last)
		picked[i] = order[n]
		order[n] = order[last-1]
	}
	order = nil
	slices.Sort(picked)
	result := make([]*meta.Meta, count)
	for i, p := range picked {
		result[i] = metaList[p]
	}
	return result
}

181
182
183
184
185
186
187
188
189
190
191
192

func limitElements(metaList []*meta.Meta, limit int) []*meta.Meta {
	if limit > 0 && limit < len(metaList) {
		return metaList[:limit]
	}
	return metaList
}

func sortMetaByZid(metaList []*meta.Meta) []*meta.Meta {
	sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
	return metaList
}







<
<
<
<
<
193
194
195
196
197
198
199






func limitElements(metaList []*meta.Meta, limit int) []*meta.Meta {
	if limit > 0 && limit < len(metaList) {
		return metaList[:limit]
	}
	return metaList
}





Changes to query/context.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

68
69
70
71
72
73
74
package query

import (
	"container/heap"
	"context"
	"math"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ContextSpec contains all specification values for calculating a context.
type ContextSpec struct {
	Direction ContextDirection
	MaxCost   int
	MaxCount  int
	Full      bool
}

// ContextDirection specifies the direction a context should be calculated.
type ContextDirection uint8


const (
	ContextDirBoth ContextDirection = iota
	ContextDirForward
	ContextDirBackward
)

// ContextPort is the collection of box methods needed by this directive.
type ContextPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error)
}


func (spec *ContextSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ContextDirective)
	if spec.Full {
		pe.printSpace()
		pe.writeString(api.FullDirective)
	}
	switch spec.Direction {
	case ContextDirBackward:
		pe.printSpace()
		pe.writeString(api.BackwardDirective)
	case ContextDirForward:
		pe.printSpace()
		pe.writeString(api.ForwardDirective)
	}
	pe.printPosInt(api.CostDirective, spec.MaxCost)
	pe.printPosInt(api.MaxDirective, spec.MaxCount)
}


func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta {
	maxCost := float64(spec.MaxCost)
	if maxCost <= 0 {
		maxCost = 17
	}
	maxCount := spec.MaxCount
	if maxCount <= 0 {







|















>












>



















>







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package query

import (
	"container/heap"
	"context"
	"math"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ContextSpec contains all specification values for calculating a context.
type ContextSpec struct {
	Direction ContextDirection
	MaxCost   int
	MaxCount  int
	Full      bool
}

// ContextDirection specifies the direction a context should be calculated.
type ContextDirection uint8

// Constants for ContextDirection.
const (
	ContextDirBoth ContextDirection = iota
	ContextDirForward
	ContextDirBackward
)

// ContextPort is the collection of box methods needed by this directive.
type ContextPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error)
}

// Print the spec on the given print environment.
func (spec *ContextSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ContextDirective)
	if spec.Full {
		pe.printSpace()
		pe.writeString(api.FullDirective)
	}
	switch spec.Direction {
	case ContextDirBackward:
		pe.printSpace()
		pe.writeString(api.BackwardDirective)
	case ContextDirForward:
		pe.printSpace()
		pe.writeString(api.ForwardDirective)
	}
	pe.printPosInt(api.CostDirective, spec.MaxCost)
	pe.printPosInt(api.MaxDirective, spec.MaxCount)
}

// Execute the specification.
func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta {
	maxCost := float64(spec.MaxCost)
	if maxCost <= 0 {
		maxCost = 17
	}
	maxCount := spec.MaxCount
	if maxCount <= 0 {
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
	old[n-1].meta = nil // avoid memory leak
	*q = old[0 : n-1]
	return item
}

type contextTask struct {
	port     ContextPort
	seen     id.Set
	queue    ztlCtxQueue
	maxCost  float64
	limit    int
	tagMetas map[string][]*meta.Meta
	tagZids  map[string]id.Set     // just the zids of tagMetas
	metaZid  map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags
}

func newQueue(startSeq []*meta.Meta, maxCost float64, limit int, port ContextPort) *contextTask {
	result := &contextTask{
		port:     port,
		seen:     id.NewSet(),
		maxCost:  maxCost,
		limit:    limit,
		tagMetas: make(map[string][]*meta.Meta),
		tagZids:  make(map[string]id.Set),
		metaZid:  make(map[id.Zid]*meta.Meta),
	}

	queue := make(ztlCtxQueue, 0, len(startSeq))
	for _, m := range startSeq {
		queue = append(queue, ztlCtxItem{cost: 1, meta: m})
	}







|




|










|







118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
	old[n-1].meta = nil // avoid memory leak
	*q = old[0 : n-1]
	return item
}

type contextTask struct {
	port     ContextPort
	seen     *id.Set
	queue    ztlCtxQueue
	maxCost  float64
	limit    int
	tagMetas map[string][]*meta.Meta
	tagZids  map[string]*id.Set    // just the zids of tagMetas
	metaZid  map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags
}

func newQueue(startSeq []*meta.Meta, maxCost float64, limit int, port ContextPort) *contextTask {
	result := &contextTask{
		port:     port,
		seen:     id.NewSet(),
		maxCost:  maxCost,
		limit:    limit,
		tagMetas: make(map[string][]*meta.Meta),
		tagZids:  make(map[string]*id.Set),
		metaZid:  make(map[id.Zid]*meta.Meta),
	}

	queue := make(ztlCtxQueue, 0, len(startSeq))
	for _, m := range startSeq {
		queue = append(queue, ztlCtxItem{cost: 1, meta: m})
	}
175
176
177
178
179
180
181
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
		ct.addIDSet(ctx, newCost, value)
	}
}

func contextCost(key string) float64 {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 1
	case api.KeySubordinates, api.KeySuperior:
		return 1.5
	case api.KeySuccessors, api.KeyPredecessor:
		return 7
	}
	return 2
}

func (ct *contextTask) addID(ctx context.Context, newCost float64, value string) {
	if zid, errParse := id.Parse(value); errParse == nil {
		if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil {
			ct.addMeta(m, newCost)
		}
	}
}

func (ct *contextTask) addMeta(m *meta.Meta, newCost float64) {
	// If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all
	// other zettel that are directly reachable, without taking the cost into account.
	// Of course, the limit ist still relevant.
	if !ct.hasLimit() && (len(ct.seen) <= 1 || ct.maxCost == 0 || newCost <= ct.maxCost) {
		if _, found := ct.seen[m.Zid]; !found {
			heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m})
		}
	}
}

func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, value string) {
	elems := meta.ListFromValue(value)
	refCost := referenceCost(newCost, len(elems))
	for _, val := range elems {
		ct.addID(ctx, refCost, val)
	}
}

func referenceCost(baseCost float64, numReferences int) float64 {
	nRefs := float64(numReferences)
	return nRefs*math.Log2(nRefs+1) + baseCost
}

func (ct *contextTask) addTags(ctx context.Context, tags []string, baseCost float64) {
	var zidSet id.Set
	for _, tag := range tags {
		zs := ct.updateTagData(ctx, tag)
		zidSet = zidSet.Copy(zs)
	}
	for _, zid := range zidSet.Sorted() { // .Sorted() to stay deterministic
		minCost := math.MaxFloat64
		costFactor := 1.1
		for _, tag := range tags {
			tagZids := ct.tagZids[tag]
			if tagZids.Contains(zid) {
				cost := tagCost(baseCost, len(tagZids))
				if cost < minCost {
					minCost = cost
				}
				costFactor /= 1.1
			}
		}
		ct.addMeta(ct.metaZid[zid], minCost*costFactor)
	}
}

func (ct *contextTask) updateTagData(ctx context.Context, tag string) id.Set {
	if _, found := ct.tagMetas[tag]; found {
		return ct.tagZids[tag]
	}
	q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
	ml, err := ct.port.SelectMeta(ctx, nil, q)
	if err != nil {
		ml = nil







|
|
|


















|
|



















|


|

|





|







|


|







178
179
180
181
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
		ct.addIDSet(ctx, newCost, value)
	}
}

func contextCost(key string) float64 {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 0.1
	case api.KeySequel, api.KeyPrequel:
		return 1.0
	case api.KeySuccessors, api.KeyPredecessor:
		return 7
	}
	return 2
}

func (ct *contextTask) addID(ctx context.Context, newCost float64, value string) {
	if zid, errParse := id.Parse(value); errParse == nil {
		if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil {
			ct.addMeta(m, newCost)
		}
	}
}

func (ct *contextTask) addMeta(m *meta.Meta, newCost float64) {
	// If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all
	// other zettel that are directly reachable, without taking the cost into account.
	// Of course, the limit ist still relevant.
	if !ct.hasLimit() && (ct.seen.Length() <= 1 || ct.maxCost == 0 || newCost <= ct.maxCost) {
		if !ct.seen.Contains(m.Zid) {
			heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m})
		}
	}
}

func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, value string) {
	elems := meta.ListFromValue(value)
	refCost := referenceCost(newCost, len(elems))
	for _, val := range elems {
		ct.addID(ctx, refCost, val)
	}
}

func referenceCost(baseCost float64, numReferences int) float64 {
	nRefs := float64(numReferences)
	return nRefs*math.Log2(nRefs+1) + baseCost
}

func (ct *contextTask) addTags(ctx context.Context, tags []string, baseCost float64) {
	var zidSet *id.Set
	for _, tag := range tags {
		zs := ct.updateTagData(ctx, tag)
		zidSet = zidSet.IUnion(zs)
	}
	zidSet.ForEach(func(zid id.Zid) {
		minCost := math.MaxFloat64
		costFactor := 1.1
		for _, tag := range tags {
			tagZids := ct.tagZids[tag]
			if tagZids.Contains(zid) {
				cost := tagCost(baseCost, tagZids.Length())
				if cost < minCost {
					minCost = cost
				}
				costFactor /= 1.1
			}
		}
		ct.addMeta(ct.metaZid[zid], minCost*costFactor)
	})
}

func (ct *contextTask) updateTagData(ctx context.Context, tag string) *id.Set {
	if _, found := ct.tagMetas[tag]; found {
		return ct.tagZids[tag]
	}
	q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
	ml, err := ct.port.SelectMeta(ctx, nil, q)
	if err != nil {
		ml = nil
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
	if ct.hasLimit() {
		return nil, -1
	}
	for len(ct.queue) > 0 {
		item := heap.Pop(&ct.queue).(ztlCtxItem)
		m := item.meta
		zid := m.Zid
		if _, found := ct.seen[zid]; found {
			continue
		}
		ct.seen.Add(zid)
		return m, item.cost
	}
	return nil, -1
}

func (ct *contextTask) hasLimit() bool {
	limit := ct.limit
	return limit > 0 && len(ct.seen) >= limit
}







|










|

277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
	if ct.hasLimit() {
		return nil, -1
	}
	for len(ct.queue) > 0 {
		item := heap.Pop(&ct.queue).(ztlCtxItem)
		m := item.meta
		zid := m.Zid
		if ct.seen.Contains(zid) {
			continue
		}
		ct.seen.Add(zid)
		return m, item.cost
	}
	return nil, -1
}

func (ct *contextTask) hasLimit() bool {
	limit := ct.limit
	return limit > 0 && ct.seen.Length() >= limit
}

Changes to query/parser.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//-----------------------------------------------------------------------------

package query

import (
	"strconv"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Parse the query specification and return a Query object.
func Parse(spec string) (q *Query) { return q.Parse(spec) }








|
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//-----------------------------------------------------------------------------

package query

import (
	"strconv"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Parse the query specification and return a Query object.
func Parse(spec string) (q *Query) { return q.Parse(spec) }

41
42
43
44
45
46
47

48
49
50
51
52
53

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

type parserState struct {
	inp *input.Input
}

func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS }
func (ps *parserState) acceptSingleKw(s string) bool {

	if ps.inp.Accept(s) && (ps.isSpace() || ps.isActionSep() || ps.mustStop()) {
		return true
	}
	return false
}
func (ps *parserState) acceptKwArgs(s string) bool {

	if ps.inp.Accept(s) && ps.isSpace() {
		ps.skipSpace()
		return true
	}
	return false
}

const (
	actionSeparatorChar       = '|'
	existOperatorChar         = '?'
	searchOperatorNotChar     = '!'
	searchOperatorEqualChar   = '='
	searchOperatorHasChar     = ':'
	searchOperatorPrefixChar  = '['
	searchOperatorSuffixChar  = ']'
	searchOperatorMatchChar   = '~'
	searchOperatorLessChar    = '<'
	searchOperatorGreaterChar = '>'
)

func (ps *parserState) parse(q *Query) *Query {

	ps.skipSpace()
	if ps.mustStop() {
		return q
	}
	inp := ps.inp
	firstPos := inp.Pos
	zidSet := id.NewSet()
	for {
		pos := inp.Pos
		zid, found := ps.scanZid()
		if !found {
			inp.SetPos(pos)
			break
		}
		if !zidSet.ContainsOrNil(zid) {
			zidSet.Add(zid)
			q = createIfNeeded(q)
			q.zids = append(q.zids, zid)
		}
		ps.skipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
		}
	}

	hasContext := false
	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {
			if hasContext {
				inp.SetPos(pos)







>
|





>
|
|



















>
|



<









|




|








|







41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

type parserState struct {
	inp *input.Input
}

func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS }
func (ps *parserState) acceptSingleKw(s string) bool {
	inp := ps.inp
	if inp.Accept(s) && (inp.IsSpace() || ps.isActionSep() || ps.mustStop()) {
		return true
	}
	return false
}
func (ps *parserState) acceptKwArgs(s string) bool {
	inp := ps.inp
	if inp.Accept(s) && inp.IsSpace() {
		inp.SkipSpace()
		return true
	}
	return false
}

const (
	actionSeparatorChar       = '|'
	existOperatorChar         = '?'
	searchOperatorNotChar     = '!'
	searchOperatorEqualChar   = '='
	searchOperatorHasChar     = ':'
	searchOperatorPrefixChar  = '['
	searchOperatorSuffixChar  = ']'
	searchOperatorMatchChar   = '~'
	searchOperatorLessChar    = '<'
	searchOperatorGreaterChar = '>'
)

func (ps *parserState) parse(q *Query) *Query {
	inp := ps.inp
	inp.SkipSpace()
	if ps.mustStop() {
		return q
	}

	firstPos := inp.Pos
	zidSet := id.NewSet()
	for {
		pos := inp.Pos
		zid, found := ps.scanZid()
		if !found {
			inp.SetPos(pos)
			break
		}
		if !zidSet.Contains(zid) {
			zidSet.Add(zid)
			q = createIfNeeded(q)
			q.zids = append(q.zids, zid)
		}
		inp.SkipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
		}
	}

	hasContext := false
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {
			if hasContext {
				inp.SetPos(pos)
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
	}
	if q != nil && len(q.directives) == 0 {
		inp.SetPos(firstPos) // No directive -> restart at beginning
		q.zids = nil
	}

	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.OrDirective) {
			q = createIfNeeded(q)
			if !q.terms[len(q.terms)-1].isEmpty() {







|







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
	}
	if q != nil && len(q.directives) == 0 {
		inp.SetPos(firstPos) // No directive -> restart at beginning
		q.zids = nil
	}

	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.OrDirective) {
			q = createIfNeeded(q)
			if !q.terms[len(q.terms)-1].isEmpty() {
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
	return q
}

func (ps *parserState) parseContext(q *Query) *Query {
	inp := ps.inp
	spec := &ContextSpec{}
	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.FullDirective) {
			spec.Full = true
			continue







|







201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
	return q
}

func (ps *parserState) parseContext(q *Query) *Query {
	inp := ps.inp
	spec := &ContextSpec{}
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.FullDirective) {
			spec.Full = true
			continue
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
}

func (ps *parserState) parseUnlinked(q *Query) *Query {
	inp := ps.inp

	spec := &UnlinkedSpec{}
	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptKwArgs(api.PhraseDirective) {
			if word := ps.scanWord(); len(word) > 0 {
				spec.words = append(spec.words, string(word))







|







266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
}

func (ps *parserState) parseUnlinked(q *Query) *Query {
	inp := ps.inp

	spec := &UnlinkedSpec{}
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptKwArgs(api.PhraseDirective) {
			if word := ps.scanWord(); len(word) > 0 {
				spec.words = append(spec.words, string(word))
340
341
342
343
344
345
346

347
348
349
350
351
352
353
354
355
356
357
	if q.limit == 0 || q.limit >= num {
		q.limit = num
	}
	return q, true
}

func (ps *parserState) parseActions(q *Query) *Query {

	ps.inp.Next()
	var words []string
	for {
		ps.skipSpace()
		word := ps.scanWord()
		if len(word) == 0 {
			break
		}
		words = append(words, string(word))
	}
	if len(words) > 0 {







>
|


|







342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
	if q.limit == 0 || q.limit >= num {
		q.limit = num
	}
	return q, true
}

func (ps *parserState) parseActions(q *Query) *Query {
	inp := ps.inp
	inp.Next()
	var words []string
	for {
		inp.SkipSpace()
		word := ps.scanWord()
		if len(word) == 0 {
			break
		}
		words = append(words, string(word))
	}
	if len(words) > 0 {
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
	}
	text, key := ps.scanSearchTextOrKey(hasOp)
	if len(key) > 0 {
		// Assert: hasOp == false
		op, hasOp = ps.scanSearchOp()
		// Assert hasOp == true
		if op == cmpExist || op == cmpNotExist {
			if ps.isSpace() || ps.isActionSep() || ps.mustStop() {
				return q.addKey(string(key), op)
			}
			ps.inp.SetPos(pos)
			hasOp = false
			text = ps.scanWord()
			key = nil
		} else {







|







374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
	}
	text, key := ps.scanSearchTextOrKey(hasOp)
	if len(key) > 0 {
		// Assert: hasOp == false
		op, hasOp = ps.scanSearchOp()
		// Assert hasOp == true
		if op == cmpExist || op == cmpNotExist {
			if inp.IsSpace() || ps.isActionSep() || ps.mustStop() {
				return q.addKey(string(key), op)
			}
			ps.inp.SetPos(pos)
			hasOp = false
			text = ps.scanWord()
			key = nil
		} else {
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
}

func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) {
	inp := ps.inp
	pos := inp.Pos
	allowKey := !hasOp

	for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() {
		if allowKey {
			switch inp.Ch {
			case searchOperatorNotChar, existOperatorChar,
				searchOperatorEqualChar, searchOperatorHasChar,
				searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar,
				searchOperatorLessChar, searchOperatorGreaterChar:
				allowKey = false
				if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) {
					return nil, key
				}
			}
		}
		inp.Next()
	}
	return inp.Src[pos:inp.Pos], nil
}

func (ps *parserState) scanWord() []byte {
	inp := ps.inp
	pos := inp.Pos
	for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() {
		inp.Next()
	}
	return inp.Src[pos:inp.Pos]
}

func (ps *parserState) scanPosInt() (int, bool) {
	word := ps.scanWord()







|




















|







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
}

func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) {
	inp := ps.inp
	pos := inp.Pos
	allowKey := !hasOp

	for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() {
		if allowKey {
			switch inp.Ch {
			case searchOperatorNotChar, existOperatorChar,
				searchOperatorEqualChar, searchOperatorHasChar,
				searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar,
				searchOperatorLessChar, searchOperatorGreaterChar:
				allowKey = false
				if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) {
					return nil, key
				}
			}
		}
		inp.Next()
	}
	return inp.Src[pos:inp.Pos], nil
}

func (ps *parserState) scanWord() []byte {
	inp := ps.inp
	pos := inp.Pos
	for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() {
		inp.Next()
	}
	return inp.Src[pos:inp.Pos]
}

func (ps *parserState) scanPosInt() (int, bool) {
	word := ps.scanWord()
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
	}
	if negate {
		return op.negate(), true
	}
	return op, true
}

func (ps *parserState) skipSpace() {
	for ps.isSpace() {
		ps.inp.Next()
	}
}

func (ps *parserState) isSpace() bool {
	switch ch := ps.inp.Ch; ch {
	case input.EOS:
		return false
	case ' ', '\t', '\n', '\r':
		return true
	default:
		return input.IsSpace(ch)
	}
}

func (ps *parserState) isActionSep() bool {
	return ps.inp.Ch == actionSeparatorChar
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<



511
512
513
514
515
516
517

















518
519
520
	}
	if negate {
		return op.negate(), true
	}
	return op, true
}


















func (ps *parserState) isActionSep() bool {
	return ps.inp.Ch == actionSeparatorChar
}

Changes to query/print.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package query

import (
	"io"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/zettel/id"
)

var op2string = map[compareOp]string{
	cmpExist:     api.ExistOperator,
	cmpNotExist:  api.ExistNotOperator,
	cmpEqual:     api.SearchOperatorEqual,







|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package query

import (
	"io"
	"strconv"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/zettel/id"
)

var op2string = map[compareOp]string{
	cmpExist:     api.ExistOperator,
	cmpNotExist:  api.ExistNotOperator,
	cmpEqual:     api.SearchOperatorEqual,
145
146
147
148
149
150
151

152
153
154
155
156
157
158
		}
		if s := val.value; s != "" {
			pe.writeString(s)
		}
	}
}


func (q *Query) Human() string {
	var sb strings.Builder
	q.PrintHuman(&sb)
	return sb.String()
}

// PrintHuman the query to a writer in a human readable form.







>







145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
		}
		if s := val.value; s != "" {
			pe.writeString(s)
		}
	}
}

// Human returns the query as a human readable string.
func (q *Query) Human() string {
	var sb strings.Builder
	q.PrintHuman(&sb)
	return sb.String()
}

// PrintHuman the query to a writer in a human readable form.

Changes to query/query.go.

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	"zettelstore.de/z/zettel/meta"
)

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {
	// Select all zettel that contains the given exact word.
	// The word must be normalized through Unicode NKFD, trimmed and not empty.
	SearchEqual(word string) id.Set

	// Select all zettel that have a word with the given prefix.
	// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchPrefix(prefix string) id.Set

	// Select all zettel that have a word with the given suffix.
	// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchSuffix(suffix string) id.Set

	// Select all zettel that contains the given string.
	// The string must be normalized through Unicode NKFD, trimmed and not empty.
	SearchContains(s string) id.Set
}

// Query specifies a mechanism for querying zettel.
type Query struct {
	// Präfixed zettel identifier.
	zids []id.Zid








|



|



|



|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	"zettelstore.de/z/zettel/meta"
)

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {
	// Select all zettel that contains the given exact word.
	// The word must be normalized through Unicode NKFD, trimmed and not empty.
	SearchEqual(word string) *id.Set

	// Select all zettel that have a word with the given prefix.
	// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchPrefix(prefix string) *id.Set

	// Select all zettel that have a word with the given suffix.
	// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchSuffix(suffix string) *id.Set

	// Select all zettel that contains the given string.
	// The string must be normalized through Unicode NKFD, trimmed and not empty.
	SearchContains(s string) *id.Set
}

// Query specifies a mechanism for querying zettel.
type Query struct {
	// Präfixed zettel identifier.
	zids []id.Zid

332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
	if q == nil {
		return false
	}
	if len(q.zids) > 0 {
		return true
	}
	if len(q.actions) > 0 {
		// Unknown, what an action will use. Example: RSS needs api.KeyPublished.
		return true
	}
	for _, term := range q.terms {
		for key := range term.keys {
			if meta.IsProperty(key) {
				return true
			}







|







332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
	if q == nil {
		return false
	}
	if len(q.zids) > 0 {
		return true
	}
	if len(q.actions) > 0 {
		// Unknown, what an action may use. For examples: KEYS action uses all metadata.
		return true
	}
	for _, term := range q.terms {
		for key := range term.keys {
			if meta.IsProperty(key) {
				return true
			}
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
			cTerm.Match = matchAlways
		}
		result.Terms = append(result.Terms, cTerm)
	}
	return result
}

func metaList2idSet(ml []*meta.Meta) id.Set {
	if ml == nil {
		return nil
	}
	result := id.NewSetCap(len(ml))
	for _, m := range ml {
		result = result.Add(m.Zid)
	}
	return result
}

func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet id.Set) CompiledTerm {
	match := ct.compileMeta() // Match might add some searches
	var pred RetrievePredicate
	if searcher != nil {
		pred = ct.retrieveIndex(searcher)
		if startSet != nil {
			if pred == nil {
				pred = startSet.ContainsOrNil
			} else {
				predSet := id.NewSetCap(len(startSet))
				for zid := range startSet {
					if pred(zid) {
						predSet = predSet.Add(zid)
					}
				}
				pred = predSet.ContainsOrNil
			}
		}
	}
	return CompiledTerm{Match: match, Retrieve: pred}
}








|










|








|
|



|







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
			cTerm.Match = matchAlways
		}
		result.Terms = append(result.Terms, cTerm)
	}
	return result
}

func metaList2idSet(ml []*meta.Meta) *id.Set {
	if ml == nil {
		return nil
	}
	result := id.NewSetCap(len(ml))
	for _, m := range ml {
		result = result.Add(m.Zid)
	}
	return result
}

func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet *id.Set) CompiledTerm {
	match := ct.compileMeta() // Match might add some searches
	var pred RetrievePredicate
	if searcher != nil {
		pred = ct.retrieveIndex(searcher)
		if startSet != nil {
			if pred == nil {
				pred = startSet.ContainsOrNil
			} else {
				predSet := id.NewSetCap(startSet.Length())
				startSet.ForEach(func(zid id.Zid) {
					if pred(zid) {
						predSet = predSet.Add(zid)
					}
				})
				pred = predSet.ContainsOrNil
			}
		}
	}
	return CompiledTerm{Match: match, Retrieve: pred}
}

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
	positives := retrievePositives(normCalls, plainCalls)
	if positives == nil {
		// No positive search for words, must contain only words for a negative search.
		// Otherwise len(search) == 0 (see above)
		negatives := retrieveNegatives(negCalls)
		return func(zid id.Zid) bool { return !negatives.ContainsOrNil(zid) }
	}
	if len(positives) == 0 {
		// Positive search didn't found anything. We can omit the negative search.
		return neverIncluded
	}
	if len(negCalls) == 0 {
		// Positive search found something, but there is no negative search.
		return positives.ContainsOrNil
	}
	negatives := retrieveNegatives(negCalls)
	if negatives == nil {
		return positives.ContainsOrNil
	}
	return func(zid id.Zid) bool {
		return positives.ContainsOrNil(zid) && !negatives.ContainsOrNil(zid)
	}
}

// Limit returns only s.GetLimit() elements of the given list.
func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta {
	if q == nil {
		return metaList
	}
	return limitElements(metaList, q.limit)
}







|





|



|


|










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
	positives := retrievePositives(normCalls, plainCalls)
	if positives == nil {
		// No positive search for words, must contain only words for a negative search.
		// Otherwise len(search) == 0 (see above)
		negatives := retrieveNegatives(negCalls)
		return func(zid id.Zid) bool { return !negatives.ContainsOrNil(zid) }
	}
	if positives.IsEmpty() {
		// Positive search didn't found anything. We can omit the negative search.
		return neverIncluded
	}
	if len(negCalls) == 0 {
		// Positive search found something, but there is no negative search.
		return positives.Contains
	}
	negatives := retrieveNegatives(negCalls)
	if negatives == nil {
		return positives.Contains
	}
	return func(zid id.Zid) bool {
		return positives.Contains(zid) && !negatives.ContainsOrNil(zid)
	}
}

// Limit returns only s.GetLimit() elements of the given list.
func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta {
	if q == nil {
		return metaList
	}
	return limitElements(metaList, q.limit)
}

Changes to query/retrieve.go.

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"zettelstore.de/z/zettel/id"
)

type searchOp struct {
	s  string
	op compareOp
}
type searchFunc func(string) id.Set
type searchCallMap map[searchOp]searchFunc

var cmpPred = map[compareOp]func(string, string) bool{
	cmpEqual:   stringEqual,
	cmpPrefix:  strings.HasPrefix,
	cmpSuffix:  strings.HasSuffix,
	cmpMatch:   strings.Contains,







|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"zettelstore.de/z/zettel/id"
)

type searchOp struct {
	s  string
	op compareOp
}
type searchFunc func(string) *id.Set
type searchCallMap map[searchOp]searchFunc

var cmpPred = map[compareOp]func(string, string) bool{
	cmpEqual:   stringEqual,
	cmpPrefix:  strings.HasPrefix,
	cmpSuffix:  strings.HasSuffix,
	cmpMatch:   strings.Contains,
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
		if _, found := plainCalls[val]; found {
			return true
		}
	}
	return false
}

func retrievePositives(normCalls, plainCalls searchCallMap) id.Set {
	if isSuperset(normCalls, plainCalls) {
		var normResult id.Set
		for c, sf := range normCalls {
			normResult = normResult.IntersectOrSet(sf(c.s))
		}
		return normResult
	}

	type searchResults map[searchOp]id.Set
	var cache searchResults
	var plainResult id.Set
	for c, sf := range plainCalls {
		result := sf(c.s)
		if _, found := normCalls[c]; found {
			if cache == nil {
				cache = make(searchResults)
			}
			cache[c] = result
		}
		plainResult = plainResult.IntersectOrSet(result)
	}
	var normResult id.Set
	for c, sf := range normCalls {
		if cache != nil {
			if result, found := cache[c]; found {
				normResult = normResult.IntersectOrSet(result)
				continue
			}
		}
		normResult = normResult.IntersectOrSet(sf(c.s))
	}
	return normResult.Copy(plainResult)
}

func isSuperset(normCalls, plainCalls searchCallMap) bool {
	for c := range plainCalls {
		if _, found := normCalls[c]; !found {
			return false
		}
	}
	return true
}

func retrieveNegatives(negCalls searchCallMap) id.Set {
	var negatives id.Set
	for val, sf := range negCalls {
		negatives = negatives.Copy(sf(val.s))
	}
	return negatives
}

func getSearchFunc(searcher Searcher, op compareOp) searchFunc {
	switch op {
	case cmpEqual:







|

|






|

|










|









|











|
|

|







100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
		if _, found := plainCalls[val]; found {
			return true
		}
	}
	return false
}

func retrievePositives(normCalls, plainCalls searchCallMap) *id.Set {
	if isSuperset(normCalls, plainCalls) {
		var normResult *id.Set
		for c, sf := range normCalls {
			normResult = normResult.IntersectOrSet(sf(c.s))
		}
		return normResult
	}

	type searchResults map[searchOp]*id.Set
	var cache searchResults
	var plainResult *id.Set
	for c, sf := range plainCalls {
		result := sf(c.s)
		if _, found := normCalls[c]; found {
			if cache == nil {
				cache = make(searchResults)
			}
			cache[c] = result
		}
		plainResult = plainResult.IntersectOrSet(result)
	}
	var normResult *id.Set
	for c, sf := range normCalls {
		if cache != nil {
			if result, found := cache[c]; found {
				normResult = normResult.IntersectOrSet(result)
				continue
			}
		}
		normResult = normResult.IntersectOrSet(sf(c.s))
	}
	return normResult.IUnion(plainResult)
}

func isSuperset(normCalls, plainCalls searchCallMap) bool {
	for c := range plainCalls {
		if _, found := normCalls[c]; !found {
			return false
		}
	}
	return true
}

func retrieveNegatives(negCalls searchCallMap) *id.Set {
	var negatives *id.Set
	for val, sf := range negCalls {
		negatives = negatives.IUnion(sf(val.s))
	}
	return negatives
}

func getSearchFunc(searcher Searcher, op compareOp) searchFunc {
	switch op {
	case cmpEqual:

Changes to query/select_test.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package query_test

import (
	"context"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func TestMatchZidNegate(t *testing.T) {
	q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense))







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package query_test

import (
	"context"
	"testing"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func TestMatchZidNegate(t *testing.T) {
	q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense))

Changes to query/sorter.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
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import (

	"strconv"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/meta"
)

type sortFunc func(i, j int) bool

func createSortFunc(order []sortOrder, ml []*meta.Meta) sortFunc {
	hasID := false
	sortFuncs := make([]sortFunc, 0, len(order)+1)
	for _, o := range order {
		sortFuncs = append(sortFuncs, createOneSortFunc(o.key, o.descending, ml))
		if o.key == api.KeyID {
			hasID = true
			break
		}
	}
	if !hasID {
		sortFuncs = append(sortFuncs, func(i, j int) bool { return ml[i].Zid > ml[j].Zid })
	}
	// return sortFuncs[0]
	if len(sortFuncs) == 1 {
		return sortFuncs[0]
	}
	return func(i, j int) bool {
		for _, sf := range sortFuncs {
			if sf(i, j) {
				return true
			}
			if sf(j, i) {
				return false
			}
		}
		return false
	}
}

func createOneSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc {


	keyType := meta.Type(key)
	if key == api.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
	}
	if keyType == meta.TypeTimestamp {
		return createSortTimestampFunc(ml, key, descending)
	}
	if keyType == meta.TypeNumber {
		return createSortNumberFunc(ml, key, descending)
	}
	return createSortStringFunc(ml, key, descending)
}



func createSortTimestampFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)



			return (iOk && (!jOk || meta.ExpandTimestamp(iVal) > meta.ExpandTimestamp(jVal))) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := ml[i].Get(key)
		jVal, jOk := ml[j].Get(key)



		return (iOk && (!jOk || meta.ExpandTimestamp(iVal) < meta.ExpandTimestamp(jVal))) || !jOk
	}
}

func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := getNum(ml[i], key)
			jVal, jOk := getNum(ml[j], key)

			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}

	}

	return func(i, j int) bool {
		iVal, iOk := getNum(ml[i], key)
		jVal, jOk := getNum(ml[j], key)

		return (iOk && (!jOk || iVal < jVal)) || !jOk


	}
}

func getNum(m *meta.Meta, key string) (int64, bool) {
	if s, ok := m.Get(key); ok {
		if i, err := strconv.ParseInt(s, 10, 64); err == nil {
			return i, true
		}
	}
	return 0, false
}

func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)

			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}

	}

	return func(i, j int) bool {
		iVal, iOk := ml[i].Get(key)
		jVal, jOk := ml[j].Get(key)










		return (iOk && (!jOk || iVal < jVal)) || !jOk
	}






}







>


|



|

|



|






|

<



|

|
|

<
<
|
<
|


<
|
>
>


|
|

|


|


|

|


>
>
|

|
|
|
>
>
>
|


|
|
|
>
>
>
|



|

|
|
|
>
|
|
>
|
>
|
|
|
>
|
>
>












|

|
|
|
>
|
|
>
|
>
|
|
|
>
>
>
>
>
>
>
>
>
>
|
|
>
>
>
>
>
>

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

39
40
41
42
43
44
45
46


47

48
49
50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import (
	"cmp"
	"strconv"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/meta"
)

type sortFunc func(i, j *meta.Meta) int

func buildSortFunc(order []sortOrder) sortFunc {
	hasID := false
	sortFuncs := make([]sortFunc, 0, len(order)+1)
	for _, o := range order {
		sortFuncs = append(sortFuncs, o.buildSortfunc())
		if o.key == api.KeyID {
			hasID = true
			break
		}
	}
	if !hasID {
		sortFuncs = append(sortFuncs, defaultMetaSort)
	}

	if len(sortFuncs) == 1 {
		return sortFuncs[0]
	}
	return func(i, j *meta.Meta) int {
		for _, sf := range sortFuncs {
			if result := sf(i, j); result != 0 {
				return result
			}


		}

		return 0
	}
}


func (so *sortOrder) buildSortfunc() sortFunc {
	key := so.key
	keyType := meta.Type(key)
	if key == api.KeyID || keyType == meta.TypeCredential {
		if so.descending {
			return defaultMetaSort
		}
		return func(i, j *meta.Meta) int { return cmp.Compare(i.Zid, j.Zid) }
	}
	if keyType == meta.TypeTimestamp {
		return createSortTimestampFunc(key, so.descending)
	}
	if keyType == meta.TypeNumber {
		return createSortNumberFunc(key, so.descending)
	}
	return createSortStringFunc(key, so.descending)
}

func defaultMetaSort(i, j *meta.Meta) int { return cmp.Compare(j.Zid, i.Zid) }

func createSortTimestampFunc(key string, descending bool) sortFunc {
	if descending {
		return func(i, j *meta.Meta) int {
			iVal, iOk := i.Get(key)
			jVal, jOk := j.Get(key)
			if result := compareFound(jOk, iOk); result != 0 {
				return result
			}
			return cmp.Compare(meta.ExpandTimestamp(jVal), meta.ExpandTimestamp(iVal))
		}
	}
	return func(i, j *meta.Meta) int {
		iVal, iOk := i.Get(key)
		jVal, jOk := j.Get(key)
		if result := compareFound(iOk, jOk); result != 0 {
			return result
		}
		return cmp.Compare(meta.ExpandTimestamp(iVal), meta.ExpandTimestamp(jVal))
	}
}

func createSortNumberFunc(key string, descending bool) sortFunc {
	if descending {
		return func(i, j *meta.Meta) int {
			iVal, iOk := getNum(i, key)
			jVal, jOk := getNum(j, key)
			if result := compareFound(jOk, iOk); result != 0 {
				return result
			}
			return cmp.Compare(jVal, iVal)
		}
	}
	return func(i, j *meta.Meta) int {
		iVal, iOk := getNum(i, key)
		jVal, jOk := getNum(j, key)
		if result := compareFound(iOk, jOk); result != 0 {
			return result
		}
		return cmp.Compare(iVal, jVal)
	}
}

func getNum(m *meta.Meta, key string) (int64, bool) {
	if s, ok := m.Get(key); ok {
		if i, err := strconv.ParseInt(s, 10, 64); err == nil {
			return i, true
		}
	}
	return 0, false
}

func createSortStringFunc(key string, descending bool) sortFunc {
	if descending {
		return func(i, j *meta.Meta) int {
			iVal, iOk := i.Get(key)
			jVal, jOk := j.Get(key)
			if result := compareFound(jOk, iOk); result != 0 {
				return result
			}
			return cmp.Compare(jVal, iVal)
		}
	}
	return func(i, j *meta.Meta) int {
		iVal, iOk := i.Get(key)
		jVal, jOk := j.Get(key)
		if result := compareFound(iOk, jOk); result != 0 {
			return result
		}
		return cmp.Compare(iVal, jVal)
	}
}

func compareFound(iOk, jOk bool) int {
	if iOk {
		if jOk {
			return 0
		}
		return 1
	}
	if jOk {
		return -1
	}
	return 0
}

Changes to query/specs.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
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import "zettelstore.de/client.fossil/api"

// IdentSpec contains all specification values to calculate the ident directive.
type IdentSpec struct{}


func (spec *IdentSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.IdentDirective)
}

// ItemsSpec contains all specification values to calculate items.
type ItemsSpec struct{}


func (spec *ItemsSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ItemsDirective)
}







|




>








>




9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import "t73f.de/r/zsc/api"

// IdentSpec contains all specification values to calculate the ident directive.
type IdentSpec struct{}

// Print the spec on the given print environment.
func (spec *IdentSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.IdentDirective)
}

// ItemsSpec contains all specification values to calculate items.
type ItemsSpec struct{}

// Print the spec on the given print environment.
func (spec *ItemsSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ItemsDirective)
}

Changes to query/unlinked.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
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import (
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

// UnlinkedSpec contains all specification values to calculate unlinked references.
type UnlinkedSpec struct {
	words []string
}


func (spec *UnlinkedSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.UnlinkedDirective)
	for _, word := range spec.words {
		pe.writeStrings(" ", api.PhraseDirective, " ", word)
	}
}


func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string {
	if words := spec.words; len(words) > 0 {
		result := make([]string, len(words))
		copy(result, words)
		return result
	}
	result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title







|









>








>







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package query

import (
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

// UnlinkedSpec contains all specification values to calculate unlinked references.
type UnlinkedSpec struct {
	words []string
}

// Print the spec on the given print environment.
func (spec *UnlinkedSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.UnlinkedDirective)
	for _, word := range spec.words {
		pe.writeStrings(" ", api.PhraseDirective, " ", word)
	}
}

// GetWords returns all title words of a given query result.
func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string {
	if words := spec.words; len(words) > 0 {
		result := make([]string, len(words))
		copy(result, words)
		return result
	}
	result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title

Changes to strfun/slugify_test.go.

34
35
36
37
38
39
40



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
		if got := strfun.Slugify(test.in); got != test.exp {
			t.Errorf("%q: %q != %q", test.in, got, test.exp)
		}
	}
}

func eqStringSlide(got, exp []string) bool {



	if len(got) != len(exp) {
		return false
	}
	for i, g := range got {
		if g != exp[i] {
			return false
		}
	}
	return true
}

func TestNormalizeWord(t *testing.T) {
	t.Parallel()
	tests := []struct {
		in  string
		exp []string
	}{
		{"", []string{}},
		{" ", []string{}},
		{"Ë‹", []string{}}, // No single diacritic char, such as U+02CB
		{"simple test", []string{"simple", "test"}},
		{"I'm a go developer", []string{"i", "m", "a", "go", "developer"}},
		{"-!->simple   test<-!-", []string{"simple", "test"}},
		{"äöüÄÖÜß", []string{"aouaouß"}},
		{"\"aèf", []string{"aef"}},
		{"a#b", []string{"a", "b"}},
		{"*", []string{}},
		{"123", []string{"123"}},
		{"1²3", []string{"123"}},
		{"Period.", []string{"period"}},
		{" WORD  NUMBER ", []string{"word", "number"}},
	}
	for _, test := range tests {
		if got := strfun.NormalizeWords(test.in); !eqStringSlide(got, test.exp) {
			t.Errorf("%q: %q != %q", test.in, got, test.exp)
		}
	}
}







>
>
>

















|
|
|






|











34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
		if got := strfun.Slugify(test.in); got != test.exp {
			t.Errorf("%q: %q != %q", test.in, got, test.exp)
		}
	}
}

func eqStringSlide(got, exp []string) bool {
	if got == nil {
		return exp == nil
	}
	if len(got) != len(exp) {
		return false
	}
	for i, g := range got {
		if g != exp[i] {
			return false
		}
	}
	return true
}

func TestNormalizeWord(t *testing.T) {
	t.Parallel()
	tests := []struct {
		in  string
		exp []string
	}{
		{"", nil},
		{" ", nil},
		{"Ë‹", nil}, // No single diacritic char, such as U+02CB
		{"simple test", []string{"simple", "test"}},
		{"I'm a go developer", []string{"i", "m", "a", "go", "developer"}},
		{"-!->simple   test<-!-", []string{"simple", "test"}},
		{"äöüÄÖÜß", []string{"aouaouß"}},
		{"\"aèf", []string{"aef"}},
		{"a#b", []string{"a", "b"}},
		{"*", nil},
		{"123", []string{"123"}},
		{"1²3", []string{"123"}},
		{"Period.", []string{"period"}},
		{" WORD  NUMBER ", []string{"word", "number"}},
	}
	for _, test := range tests {
		if got := strfun.NormalizeWords(test.in); !eqStringSlide(got, test.exp) {
			t.Errorf("%q: %q != %q", test.in, got, test.exp)
		}
	}
}

Added testdata/testbox/00000999999999.zettel.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
id: 00000999999999
title: Zettelstore Application Directory
role: configuration
syntax: none
app-zid: 00000999999999
created: 20240703235900
lang: en
modified: 20240708125724
nozid-zid: 9999999998
noappzid: 00000999999999
visibility: login

Changes to tests/client/client_test.go.

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	"io"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/client"
	"zettelstore.de/z/kernel"
)

func nextZid(zid api.ZettelID) api.ZettelID {
	numVal, err := strconv.ParseUint(string(zid), 10, 64)
	if err != nil {
		panic(err)







|
|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	"io"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
	"zettelstore.de/z/kernel"
)

func nextZid(zid api.ZettelID) api.ZettelID {
	numVal, err := strconv.ParseUint(string(zid), 10, 64)
	if err != nil {
		panic(err)
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 55
		configRoleZettel = 33
		writerZettel     = ownerZettel - 24
		readerZettel     = ownerZettel - 24
		creatorZettel    = 10
		publicZettel     = 5
	)

	testdata := []struct {
		user string
		exp  int
	}{
		{"", publicZettel},







|
|
|
|
|
|







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 58
		configRoleZettel = 36
		writerZettel     = ownerZettel - 25
		readerZettel     = ownerZettel - 25
		creatorZettel    = 11
		publicZettel     = 6
	)

	testdata := []struct {
		user string
		exp  int
	}{
		{"", publicZettel},
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
	}
	checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel)
	checkListZid(t, metaSeq, 1, api.ZidTemplateNewRole)
	checkListZid(t, metaSeq, 2, api.ZidTemplateNewTag)
	checkListZid(t, metaSeq, 3, api.ZidTemplateNewUser)
}

// func TestGetZettelContext(t *testing.T) {
// 	const (
// 		allUserZid = api.ZettelID("20211019200500")
// 		ownerZid   = api.ZettelID("20210629163300")
// 		writerZid  = api.ZettelID("20210629165000")
// 		readerZid  = api.ZettelID("20210629165024")
// 		creatorZid = api.ZettelID("20210629165050")
// 		limitAll   = 3
// 	)

// 	t.Parallel()
// 	c := getClient()
// 	c.SetAuth("owner", "owner")
// 	rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll)
// 	if err != nil {
// 		t.Error(err)
// 		return
// 	}

// 	if !checkZid(t, ownerZid, rl.ID) {
// 		return
// 	}
// 	l := rl.List
// 	if got := len(l); got != limitAll {
// 		t.Errorf("Expected list of length %d, got %d", limitAll, got)
// 		t.Error(rl)
// 		return
// 	}

// 	checkListZid(t, l, 0, allUserZid)
// 	// checkListZid(t, l, 1, writerZid)
// 	// checkListZid(t, l, 2, readerZid)
// 	checkListZid(t, l, 1, creatorZid)

// 	rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0)

// 	if err != nil {

// 		t.Error(err)
// 		return
// 	}
// 	if !checkZid(t, ownerZid, rl.ID) {
// 		return
// 	}

// 	l = rl.List
// 	if got, exp := len(l), 4; got != exp {
// 		t.Errorf("Expected list of length %d, got %d", exp, got)
// 		return
// 	}
// 	checkListZid(t, l, 0, allUserZid)
// }




func TestGetUnlinkedReferences(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
	if err != nil {







|
|
|
|
|
|
|
|
<
>
|
|
|
|
|
|
|
<
>
|
|
|
|
<
<
|
|
<
>
|
<
<
<
|
<
>
|
>
|
|
<
<
<
<
>
|
|
|
<
<
<
<
>
>
>







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
	}
	checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel)
	checkListZid(t, metaSeq, 1, api.ZidTemplateNewRole)
	checkListZid(t, metaSeq, 2, api.ZidTemplateNewTag)
	checkListZid(t, metaSeq, 3, api.ZidTemplateNewUser)
}

func TestGetZettelContext(t *testing.T) {
	const (
		allUserZid = api.ZettelID("20211019200500")
		ownerZid   = api.ZettelID("20210629163300")
		writerZid  = api.ZettelID("20210629165000")
		readerZid  = api.ZettelID("20210629165024")
		creatorZid = api.ZettelID("20210629165050")
		limitAll   = 3

	)
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.QueryZettel(context.Background(), string(ownerZid)+" CONTEXT LIMIT "+strconv.Itoa(limitAll))
	if err != nil {
		t.Error(err)
		return

	}
	checkZidList(t, []api.ZettelID{ownerZid, allUserZid, writerZid}, rl)

	rl, err = c.QueryZettel(context.Background(), string(ownerZid)+" CONTEXT BACKWARD")
	if err != nil {


		t.Error(err)
		return

	}
	checkZidList(t, []api.ZettelID{ownerZid, allUserZid}, rl)



}

func checkZidList(t *testing.T, exp []api.ZettelID, got [][]byte) {
	t.Helper()
	if len(exp) != len(got) {
		t.Errorf("expected a list fo length %d, but got %d", len(exp), len(got))
		return




	}
	for i, expZid := range exp {
		if gotZid := api.ZettelID(got[i][:14]); expZid != gotZid {
			t.Errorf("lists differ at pos %d: expected id %v, but got %v", i, expZid, gotZid)




		}
	}
}

func TestGetUnlinkedReferences(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
	if err != nil {
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
		t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid)
	}
}

func TestRedirect(t *testing.T) {
	t.Parallel()
	c := getClient()
	search := api.OrderDirective + " " + api.ReverseDirective + " " + api.KeyID + api.ActionSeparator + api.RedirectAction
	ub := c.NewURLBuilder('z').AppendQuery(search)
	respRedirect, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respRedirect.Body.Close()







|







415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
		t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid)
	}
}

func TestRedirect(t *testing.T) {
	t.Parallel()
	c := getClient()
	search := "emoji" + api.ActionSeparator + api.RedirectAction
	ub := c.NewURLBuilder('z').AppendQuery(search)
	respRedirect, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respRedirect.Body.Close()
467
468
469
470
471
472
473



















474
475
476
477
478
479
480
		t.Error(err)
		return
	}
	if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" {
		t.Error(ver)
	}
}




















var baseURL string

func init() {
	flag.StringVar(&baseURL, "base-url", "", "Base URL")
}








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
		t.Error(err)
		return
	}
	if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" {
		t.Error(ver)
	}
}

func TestApplicationZid(t *testing.T) {
	c := getClient()
	c.SetAuth("reader", "reader")
	zid, err := c.GetApplicationZid(context.Background(), "app")
	if err != nil {
		t.Error(err)
		return
	}
	if zid != api.ZidAppDirectory {
		t.Errorf("c.GetApplicationZid(\"app\") should result in %q, but got: %q", api.ZidAppDirectory, zid)
	}
	if zid, err = c.GetApplicationZid(context.Background(), "noappzid"); err == nil {
		t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid)
	}
	if zid, err = c.GetApplicationZid(context.Background(), "nozid"); err == nil {
		t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid)
	}
}

var baseURL string

func init() {
	flag.StringVar(&baseURL, "base-url", "", "Base URL")
}

Changes to tests/client/crud_test.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package client_test

import (
	"context"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/client"
)

// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.

func TestCreateGetRenameDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	zettel := `title: A Test

Example content.`
	c := getClient()
	c.SetAuth("owner", "owner")
	zid, err := c.CreateZettel(context.Background(), []byte(zettel))







|
|





|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package client_test

import (
	"context"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
)

// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.

func TestCreateGetDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	zettel := `title: A Test

Example content.`
	c := getClient()
	c.SetAuth("owner", "owner")
	zid, err := c.CreateZettel(context.Background(), []byte(zettel))
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
	}
	exp := `title: A Test

Example content.`
	if string(data) != exp {
		t.Errorf("Expected zettel data: %q, but got %q", exp, data)
	}
	newZid := nextZid(zid)
	err = c.RenameZettel(context.Background(), zid, newZid)
	if err != nil {
		t.Error("Cannot rename", zid, ":", err)
		newZid = zid
	}

	doDelete(t, c, newZid)
}

func TestCreateGetRenameDeleteZettelData(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("creator", "creator")
	zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
		Meta:     nil,
		Encoding: "",
		Content:  "Example",
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	if !zid.IsValid() {
		t.Error("Invalid zettel ID", zid)
		return
	}
	newZid := nextZid(zid)
	c.SetAuth("owner", "owner")
	err = c.RenameZettel(context.Background(), zid, newZid)
	if err != nil {
		t.Error("Cannot rename", zid, ":", err)
		newZid = zid
	}

	c.SetAuth("owner", "owner")
	doDelete(t, c, newZid)
}

func TestCreateGetDeleteZettelData(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("owner", "owner")
	wrongModified := "19691231115959"







<
<
<
<
<
|
<
|


|
















<
<
<
<
<
<
|
<

|







48
49
50
51
52
53
54





55

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75






76

77
78
79
80
81
82
83
84
85
	}
	exp := `title: A Test

Example content.`
	if string(data) != exp {
		t.Errorf("Expected zettel data: %q, but got %q", exp, data)
	}







	doDelete(t, c, zid)
}

func TestCreateGetDeleteZettelDataCreator(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("creator", "creator")
	zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
		Meta:     nil,
		Encoding: "",
		Content:  "Example",
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	if !zid.IsValid() {
		t.Error("Invalid zettel ID", zid)
		return
	}








	c.SetAuth("owner", "owner")
	doDelete(t, c, zid)
}

func TestCreateGetDeleteZettelData(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("owner", "owner")
	wrongModified := "19691231115959"

Changes to tests/client/embed_test.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package client_test

import (
	"context"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
)

const (
	abcZid   = api.ZettelID("20211020121000")
	abc10Zid = api.ZettelID("20211020121100")
)








|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package client_test

import (
	"context"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
)

const (
	abcZid   = api.ZettelID("20211020121000")
	abc10Zid = api.ZettelID("20211020121100")
)

Changes to tests/markdown_test.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/mdenc"
	_ "zettelstore.de/z/encoder/shtmlenc"
	_ "zettelstore.de/z/encoder/szenc"







|
|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/mdenc"
	_ "zettelstore.de/z/encoder/shtmlenc"
	_ "zettelstore.de/z/encoder/szenc"
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
	return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi)
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {







|







84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
	return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi)
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) {
			encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {

Changes to tests/naughtystrings_test.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import (
	"bufio"
	"io"
	"os"
	"path/filepath"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	_ "zettelstore.de/z/cmd"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

// Test all parser / encoder with a list of "naughty strings", i.e. unusual strings







|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import (
	"bufio"
	"io"
	"os"
	"path/filepath"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	_ "zettelstore.de/z/cmd"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

// Test all parser / encoder with a list of "naughty strings", i.e. unusual strings

Changes to tests/regression_test.go.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"







|







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"

Changes to tools/build/build.go.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/tools"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func readVersionFile() (string, error) {







|
|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/tools"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func readVersionFile() (string, error) {
245
246
247
248
249
250
251
252
253
254

255
256
257
258
259
260
261
		arch string
		os   string
		env  []string
		name string
	}{
		{"amd64", "linux", nil, "zettelstore"},
		{"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
		{"amd64", "darwin", nil, "zettelstore"},
		{"arm64", "darwin", nil, "zettelstore"},
		{"amd64", "windows", nil, "zettelstore.exe"},

	}
	for _, rel := range releases {
		env := append([]string{}, rel.env...)
		env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
		env = append(env, tools.EnvDirectProxy...)
		env = append(env, tools.EnvGoVCS...)
		zsName := filepath.Join("releases", rel.name)







|
|

>







245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
		arch string
		os   string
		env  []string
		name string
	}{
		{"amd64", "linux", nil, "zettelstore"},
		{"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
		{"arm64", "darwin", nil, "zettelstore"},
		{"amd64", "darwin", nil, "zettelstore"},
		{"amd64", "windows", nil, "zettelstore.exe"},
		{"arm64", "android", nil, "zettelstore"},
	}
	for _, rel := range releases {
		env := append([]string{}, rel.env...)
		env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
		env = append(env, tools.EnvDirectProxy...)
		env = append(env, tools.EnvGoVCS...)
		zsName := filepath.Join("releases", rel.name)

Changes to tools/devtools/devtools.go.

35
36
37
38
39
40
41

42
43
44
45
46
47
48
	tools := []struct{ name, pack string }{
		{"shadow", "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest"},
		{"unparam", "mvdan.cc/unparam@latest"},
		{"staticcheck", "honnef.co/go/tools/cmd/staticcheck@latest"},
		{"govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest"},
		{"deadcode", "golang.org/x/tools/cmd/deadcode@latest"},
		{"errcheck", "github.com/kisielk/errcheck@latest"},

	}
	for _, tool := range tools {
		err := doGoInstall(tool.pack)
		if err != nil {
			return err
		}
	}







>







35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	tools := []struct{ name, pack string }{
		{"shadow", "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest"},
		{"unparam", "mvdan.cc/unparam@latest"},
		{"staticcheck", "honnef.co/go/tools/cmd/staticcheck@latest"},
		{"govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest"},
		{"deadcode", "golang.org/x/tools/cmd/deadcode@latest"},
		{"errcheck", "github.com/kisielk/errcheck@latest"},
		{"revive", "github.com/mgechev/revive@latest"},
	}
	for _, tool := range tools {
		err := doGoInstall(tool.pack)
		if err != nil {
			return err
		}
	}

Changes to tools/htmllint/htmllint.go.

1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------


package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"math/rand/v2"
	"net/url"
	"os"
	"regexp"
	"sort"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/client"
	"zettelstore.de/z/tools"
)

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()














>











|


|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

// Package main provides a tool to check the validity of HTML zettel documents.
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"math/rand/v2"
	"net/url"
	"os"
	"regexp"
	"slices"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
	"zettelstore.de/z/tools"
)

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
		msgCount := 0
		fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text)
		for _, zid := range zidsToUse(zids, perm, kd.sampleSize) {
			var nmsgs int
			nmsgs, err = validateHTML(client, kd.uc, api.ZettelID(zid))
			if err != nil {
				fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err)
				msgCount += 1
			} else {
				msgCount += nmsgs
			}
		}
		if msgCount == 1 {
			fmt.Fprintln(os.Stderr, "==> found 1 possible issue")
		} else if msgCount > 1 {
			fmt.Fprintf(os.Stderr, "==> found %v possible issues\n", msgCount)
		}
	}
	return nil
}

func calculateZids(metaList []api.ZidMetaRights) ([]string, []int) {
	zids := make([]string, len(metaList))
	for i, m := range metaList {
		zids[i] = string(m.ID)
	}
	sort.Strings(zids)
	return zids, rand.Perm(len(metaList))
}

func zidsToUse(zids []string, perm []int, sampleSize int) []string {
	if sampleSize < 0 || len(perm) <= sampleSize {
		return zids
	}
	if sampleSize == 0 {
		return nil
	}
	result := make([]string, sampleSize)
	for i := range sampleSize {
		result[i] = zids[perm[i]]
	}
	sort.Strings(result)
	return result
}

var keyDescr = []struct {
	uc         urlCreator
	text       string
	sampleSize int
}{
	{getHTMLZettel, "zettel HTML encoding", -1},
	{createJustKey('h'), "zettel web view", -1},
	{createJustKey('i'), "zettel info view", -1},
	{createJustKey('e'), "zettel edit form", 100},
	{createJustKey('c'), "zettel create form", 10},
	{createJustKey('b'), "zettel rename form", 100},
	{createJustKey('d'), "zettel delete dialog", 200},
}

type urlCreator func(*client.Client, api.ZettelID) *api.URLBuilder

func createJustKey(key byte) urlCreator {
	return func(c *client.Client, zid api.ZettelID) *api.URLBuilder {







|


















|














|













<







58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

113
114
115
116
117
118
119
		msgCount := 0
		fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text)
		for _, zid := range zidsToUse(zids, perm, kd.sampleSize) {
			var nmsgs int
			nmsgs, err = validateHTML(client, kd.uc, api.ZettelID(zid))
			if err != nil {
				fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err)
				msgCount++
			} else {
				msgCount += nmsgs
			}
		}
		if msgCount == 1 {
			fmt.Fprintln(os.Stderr, "==> found 1 possible issue")
		} else if msgCount > 1 {
			fmt.Fprintf(os.Stderr, "==> found %v possible issues\n", msgCount)
		}
	}
	return nil
}

func calculateZids(metaList []api.ZidMetaRights) ([]string, []int) {
	zids := make([]string, len(metaList))
	for i, m := range metaList {
		zids[i] = string(m.ID)
	}
	slices.Sort(zids)
	return zids, rand.Perm(len(metaList))
}

func zidsToUse(zids []string, perm []int, sampleSize int) []string {
	if sampleSize < 0 || len(perm) <= sampleSize {
		return zids
	}
	if sampleSize == 0 {
		return nil
	}
	result := make([]string, sampleSize)
	for i := range sampleSize {
		result[i] = zids[perm[i]]
	}
	slices.Sort(result)
	return result
}

var keyDescr = []struct {
	uc         urlCreator
	text       string
	sampleSize int
}{
	{getHTMLZettel, "zettel HTML encoding", -1},
	{createJustKey('h'), "zettel web view", -1},
	{createJustKey('i'), "zettel info view", -1},
	{createJustKey('e'), "zettel edit form", 100},
	{createJustKey('c'), "zettel create form", 10},

	{createJustKey('d'), "zettel delete dialog", 200},
}

type urlCreator func(*client.Client, api.ZettelID) *api.URLBuilder

func createJustKey(key byte) urlCreator {
	return func(c *client.Client, zid api.ZettelID) *api.URLBuilder {

Changes to tools/tools.go.

22
23
24
25
26
27
28


29
30



31
32

33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57
58
59


60
61
62
63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85



86
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
	"os"
	"os/exec"
	"strings"

	"zettelstore.de/z/strfun"
)



var EnvDirectProxy = []string{"GOPROXY=direct"}
var EnvGoVCS = []string{"GOVCS=zettelstore.de:fossil"}



var Verbose bool


func ExecuteCommand(env []string, name string, arg ...string) (string, error) {
	LogCommand("EXEC", env, name, arg)
	var out strings.Builder
	cmd := PrepareCommand(env, name, arg, nil, &out, os.Stderr)
	err := cmd.Run()
	return out.String(), err
}


func ExecuteFilter(data []byte, env []string, name string, arg ...string) (string, string, error) {
	LogCommand("EXEC", env, name, arg)
	var stdout, stderr strings.Builder
	cmd := PrepareCommand(env, name, arg, bytes.NewReader(data), &stdout, &stderr)
	err := cmd.Run()
	return stdout.String(), stderr.String(), err
}


func PrepareCommand(env []string, name string, arg []string, in io.Reader, stdout, stderr io.Writer) *exec.Cmd {
	if len(env) > 0 {
		env = append(env, os.Environ()...)
	}
	cmd := exec.Command(name, arg...)
	cmd.Env = env
	cmd.Stdin = in
	cmd.Stdout = stdout
	cmd.Stderr = stderr
	return cmd
}


func LogCommand(exec string, env []string, name string, arg []string) {
	if Verbose {
		if len(env) > 0 {
			for i, e := range env {
				fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e)
			}
		}
		fmt.Fprintln(os.Stderr, exec, name, arg)
	}
}


func Check(forRelease bool) error {
	if err := CheckGoTest("./..."); err != nil {
		return err
	}
	if err := checkGoVet(); err != nil {
		return err
	}
	if err := checkShadow(forRelease); err != nil {
		return err
	}
	if err := checkStaticcheck(); err != nil {
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err



	}
	if forRelease {
		if err := checkGoVulncheck(); err != nil {
			return err
		}
	}
	return checkFossilExtra()
}


func CheckGoTest(pkg string, testParams ...string) error {
	var env []string
	env = append(env, EnvDirectProxy...)
	env = append(env, EnvGoVCS...)
	args := []string{"test", pkg}
	args = append(args, testParams...)
	out, err := ExecuteCommand(env, "go", args...)







>
>
|
|
>
>
>


>








>








>











>
>











>















>
>
>









>







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
	"os"
	"os/exec"
	"strings"

	"zettelstore.de/z/strfun"
)

// Some constants to make Go work with fossil.
var (
	EnvDirectProxy = []string{"GOPROXY=direct"}
	EnvGoVCS       = []string{"GOVCS=zettelstore.de:fossil,t73f.de:fossil"}
)

// Verbose signals a verbose tool execution.
var Verbose bool

// ExecuteCommand executes a specific command.
func ExecuteCommand(env []string, name string, arg ...string) (string, error) {
	LogCommand("EXEC", env, name, arg)
	var out strings.Builder
	cmd := PrepareCommand(env, name, arg, nil, &out, os.Stderr)
	err := cmd.Run()
	return out.String(), err
}

// ExecuteFilter executes an external program to be used as a filter.
func ExecuteFilter(data []byte, env []string, name string, arg ...string) (string, string, error) {
	LogCommand("EXEC", env, name, arg)
	var stdout, stderr strings.Builder
	cmd := PrepareCommand(env, name, arg, bytes.NewReader(data), &stdout, &stderr)
	err := cmd.Run()
	return stdout.String(), stderr.String(), err
}

// PrepareCommand creates a commands to be executed.
func PrepareCommand(env []string, name string, arg []string, in io.Reader, stdout, stderr io.Writer) *exec.Cmd {
	if len(env) > 0 {
		env = append(env, os.Environ()...)
	}
	cmd := exec.Command(name, arg...)
	cmd.Env = env
	cmd.Stdin = in
	cmd.Stdout = stdout
	cmd.Stderr = stderr
	return cmd
}

// LogCommand logs the execution of a command.
func LogCommand(exec string, env []string, name string, arg []string) {
	if Verbose {
		if len(env) > 0 {
			for i, e := range env {
				fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e)
			}
		}
		fmt.Fprintln(os.Stderr, exec, name, arg)
	}
}

// Check the source with some linters.
func Check(forRelease bool) error {
	if err := CheckGoTest("./..."); err != nil {
		return err
	}
	if err := checkGoVet(); err != nil {
		return err
	}
	if err := checkShadow(forRelease); err != nil {
		return err
	}
	if err := checkStaticcheck(); err != nil {
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	if err := checkRevive(); err != nil {
		return err
	}
	if forRelease {
		if err := checkGoVulncheck(); err != nil {
			return err
		}
	}
	return checkFossilExtra()
}

// CheckGoTest runs all internal unti tests.
func CheckGoTest(pkg string, testParams ...string) error {
	var env []string
	env = append(env, EnvDirectProxy...)
	env = append(env, EnvGoVCS...)
	args := []string{"test", pkg}
	args = append(args, testParams...)
	out, err := ExecuteCommand(env, "go", args...)
138
139
140
141
142
143
144











145
146
147
148
149
150
151
func checkStaticcheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "staticcheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}











	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {







>
>
>
>
>
>
>
>
>
>
>







153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
func checkStaticcheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "staticcheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkRevive() error {
	out, err := ExecuteCommand(EnvGoVCS, "revive", "./...")
	if err != nil || out != "" {
		fmt.Fprintln(os.Stderr, "Some revive problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {

Changes to usecase/authenticate.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"context"
	"math/rand/v2"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)








|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"context"
	"math/rand/v2"
	"net/http"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

Changes to usecase/create_zettel.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package usecase

import (
	"context"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)








|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package usecase

import (
	"context"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
		m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareChild the zettel for further modification.
func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeySuperior, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta







|
|



|


|







79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
		m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareSequel the zettel for further modification.
func (*CreateZettel) PrepareSequel(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Sequel", "Sequel of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeyPrequel, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta

Changes to usecase/get_special_zettel.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that

Changes to usecase/get_user.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

Changes to usecase/lists.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// -------- List syntax ------------------------------------------------------







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// -------- List syntax ------------------------------------------------------

Changes to usecase/query.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"
120
121
122
123
124
125
126
127





128
129
130
131
132
133
134
func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
		if err != nil {
			continue
		}
		for _, ref := range collect.Order(zn) {





			if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
				if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {
					result = append(result, z.Meta)
				}
			}
		}
	}







|
>
>
>
>
>







120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
		if err != nil {
			continue
		}
		for _, ln := range collect.Order(zn) {
			ref := ln.Ref
			if !ref.IsZettel() {
				continue
			}

			if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
				if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {
					result = append(result, z.Meta)
				}
			}
		}
	}
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
			}
		}
	}
	candidates = filterByZid(candidates, refZids)
	return uc.filterCandidates(ctx, candidates, words)
}

func filterByZid(candidates []*meta.Meta, ignoreSeq id.Set) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
	for _, m := range candidates {
		if !ignoreSeq.ContainsOrNil(m.Zid) {
			result = append(result, m)
		}
	}
	return result
}

func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {







|


|







175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
			}
		}
	}
	candidates = filterByZid(candidates, refZids)
	return uc.filterCandidates(ctx, candidates, words)
}

func filterByZid(candidates []*meta.Meta, ignoreSeq *id.Set) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
	for _, m := range candidates {
		if !ignoreSeq.Contains(m.Zid) {
			result = append(result, m)
		}
	}
	return result
}

func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
	var result []string
	var curList []string
	for _, in := range *is {
		switch n := in.(type) {
		case *ast.TextNode:
			curList = append(curList, strfun.MakeWords(n.Text)...)
		case *ast.SpaceNode:
		default:
			if curList != nil {
				result = append(result, v.joinWords(curList))
				curList = nil
			}
		}
	}
	if curList != nil {
		result = append(result, v.joinWords(curList))
	}
	return result
}







<












265
266
267
268
269
270
271

272
273
274
275
276
277
278
279
280
281
282
283
func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
	var result []string
	var curList []string
	for _, in := range *is {
		switch n := in.(type) {
		case *ast.TextNode:
			curList = append(curList, strfun.MakeWords(n.Text)...)

		default:
			if curList != nil {
				result = append(result, v.joinWords(curList))
				curList = nil
			}
		}
	}
	if curList != nil {
		result = append(result, v.joinWords(curList))
	}
	return result
}

Deleted usecase/rename_zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"zettelstore.de/z/box"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// RenameZettelPort is the interface used by this use case.
type RenameZettelPort interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
	RenameZettel(ctx context.Context, curZid, newZid id.Zid) error
}

// RenameZettel is the data for this use case.
type RenameZettel struct {
	log  *logger.Logger
	port RenameZettelPort
}

// ErrZidInUse is returned if the zettel id is not appropriate for the box operation.
type ErrZidInUse struct{ Zid id.Zid }

func (err ErrZidInUse) Error() string {
	return "Zettel id already in use: " + err.Zid.String()
}

// NewRenameZettel creates a new use case.
func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel {
	return RenameZettel{log: log, port: port}
}

// Run executes the use case.
func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
	noEnrichCtx := box.NoEnrichContext(ctx)
	if _, err := uc.port.GetZettel(noEnrichCtx, curZid); err != nil {
		return err
	}
	if newZid == curZid {
		// Nothing to do
		return nil
	}
	if _, err := uc.port.GetZettel(noEnrichCtx, newZid); err == nil {
		return ErrZidInUse{Zid: newZid}
	}
	err := uc.port.RenameZettel(ctx, curZid, newZid)
	uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel")
	return err
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































































Changes to usecase/update_zettel.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)








|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

Changes to web/adapter/adapter.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

// Package adapter provides handlers for web requests, and some helper tools.
package adapter

import (
	"context"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/meta"
)

// TryReIndex executes a re-index if the appropriate query action is given.
func TryReIndex(ctx context.Context, actions []string, metaSeq []*meta.Meta, reIndex *usecase.ReIndex) ([]string, error) {
	if lenActions := len(actions); lenActions > 0 {







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

// Package adapter provides handlers for web requests, and some helper tools.
package adapter

import (
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/meta"
)

// TryReIndex executes a re-index if the appropriate query action is given.
func TryReIndex(ctx context.Context, actions []string, metaSeq []*meta.Meta, reIndex *usecase.ReIndex) ([]string, error) {
	if lenActions := len(actions); lenActions > 0 {

Changes to web/adapter/api/api.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import (
	"bytes"
	"context"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"







|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import (
	"bytes"
	"context"
	"net/http"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
	}
	if pol.CanRead(user, m) {
		result |= api.ZettelCanRead
	}
	if pol.CanWrite(user, m, m) {
		result |= api.ZettelCanWrite
	}
	if pol.CanRename(user, m) {
		result |= api.ZettelCanRename
	}
	if pol.CanDelete(user, m) {
		result |= api.ZettelCanDelete
	}
	if result == 0 {
		return api.ZettelCanNone
	}
	return result
}







<
<
<








92
93
94
95
96
97
98



99
100
101
102
103
104
105
106
	}
	if pol.CanRead(user, m) {
		result |= api.ZettelCanRead
	}
	if pol.CanWrite(user, m, m) {
		result |= api.ZettelCanWrite
	}



	if pol.CanDelete(user, m) {
		result |= api.ZettelCanDelete
	}
	if result == 0 {
		return api.ZettelCanNone
	}
	return result
}

Changes to web/adapter/api/command.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

package api

import (
	"context"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/usecase"
)

// MakePostCommandHandler creates a new HTTP handler to execute certain commands.
func (a *API) MakePostCommandHandler(
	ucIsAuth *usecase.IsAuthenticated,
	ucRefresh *usecase.Refresh,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) {
		case api.CommandAuthenticated:
			handleIsAuthenticated(ctx, w, ucIsAuth)
			return
		case api.CommandRefresh:
			err := ucRefresh.Run(ctx)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
			w.WriteHeader(http.StatusNoContent)
			return
		}
		http.Error(w, "Unknown command", http.StatusBadRequest)
	}
}

func handleIsAuthenticated(ctx context.Context, w http.ResponseWriter, ucIsAuth *usecase.IsAuthenticated) {
	switch ucIsAuth.Run(ctx) {
	case usecase.IsAuthenticatedDisabled:
		w.WriteHeader(http.StatusOK)
	case usecase.IsAuthenticatedAndValid:







|







|
|















|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

package api

import (
	"context"
	"net/http"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/usecase"
)

// MakePostCommandHandler creates a new HTTP handler to execute certain commands.
func (a *API) MakePostCommandHandler(
	ucIsAuth *usecase.IsAuthenticated,
	ucRefresh *usecase.Refresh,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) {
		case api.CommandAuthenticated:
			handleIsAuthenticated(ctx, w, ucIsAuth)
			return
		case api.CommandRefresh:
			err := ucRefresh.Run(ctx)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
			w.WriteHeader(http.StatusNoContent)
			return
		}
		http.Error(w, "Unknown command", http.StatusBadRequest)
	})
}

func handleIsAuthenticated(ctx context.Context, w http.ResponseWriter, ucIsAuth *usecase.IsAuthenticated) {
	switch ucIsAuth.Run(ctx) {
	case usecase.IsAuthenticatedDisabled:
		w.WriteHeader(http.StatusOK)
	case usecase.IsAuthenticatedAndValid:

Changes to web/adapter/api/create_zettel.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//-----------------------------------------------------------------------------

package api

import (
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		enc, encStr := getEncoding(r, q)
		var zettel zettel.Zettel
		var err error
		switch enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, id.Invalid)







|
|









|
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//-----------------------------------------------------------------------------

package api

import (
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		enc, encStr := getEncoding(r, q)
		var zettel zettel.Zettel
		var err error
		switch enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, id.Invalid)
70
71
72
73
74
75
76
77
78

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		if _, err = w.Write(result); err != nil {
			a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel")
		}
	}
}







|

70
71
72
73
74
75
76
77
78

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		if _, err = w.Write(result); err != nil {
			a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel")
		}
	})
}

Changes to web/adapter/api/delete_zettel.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
	"net/http"

	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}







|
|











|

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
	"net/http"

	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})
}

Changes to web/adapter/api/get_data.go.

12
13
14
15
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 api

import (
	"net/http"

	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeGetDataHandler creates a new HTTP handler to return zettelstore data.
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		version := ucVersion.Run()
		err := a.writeObject(w, id.Invalid, sx.MakeList(
			sx.Int64(version.Major),
			sx.Int64(version.Minor),
			sx.Int64(version.Patch),
			sx.String(version.Info),
			sx.String(version.Hash),
		))
		if err != nil {
			a.log.Error().Err(err).Msg("Write Version Info")
		}
	}
}







|





|
|





|
|




|

12
13
14
15
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 api

import (
	"net/http"

	"t73f.de/r/sx"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeGetDataHandler creates a new HTTP handler to return zettelstore data.
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		version := ucVersion.Run()
		err := a.writeObject(w, id.Invalid, sx.MakeList(
			sx.Int64(version.Major),
			sx.Int64(version.Minor),
			sx.Int64(version.Patch),
			sx.MakeString(version.Info),
			sx.MakeString(version.Hash),
		))
		if err != nil {
			a.log.Error().Err(err).Msg("Write Version Info")
		}
	})
}

Changes to web/adapter/api/get_zettel.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37




38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

import (
	"bytes"
	"context"
	"fmt"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings.
func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc {




	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()
		part := getPart(q, partContent)
		ctx := r.Context()
		switch enc, encStr := getEncoding(r, q); enc {
		case api.EncoderPlain:
			a.writePlainData(w, ctx, zid, part, getZettel)

		case api.EncoderData:
			a.writeSzData(w, ctx, zid, part, getZettel)

		default:
			var zn *ast.ZettelNode
			var em func(value string) ast.InlineSlice
			if q.Has(api.QueryKeyParseOnly) {
				zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
				em = parser.ParseMetadata
			} else {
				zn, err = evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
				em = func(value string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, value)
				}
			}
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
			a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part)
		}
	}
}

func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var contentType string
	var err error

	z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
	if err != nil {
		a.reportUsecaseError(w, err)







|
|
|












|
>
>
>
>
|











|


|



















|


|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

import (
	"bytes"
	"context"
	"fmt"
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings.
func (a *API) MakeGetZettelHandler(
	getZettel usecase.GetZettel,
	parseZettel usecase.ParseZettel,
	evaluate usecase.Evaluate,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()
		part := getPart(q, partContent)
		ctx := r.Context()
		switch enc, encStr := getEncoding(r, q); enc {
		case api.EncoderPlain:
			a.writePlainData(ctx, w, zid, part, getZettel)

		case api.EncoderData:
			a.writeSzData(ctx, w, zid, part, getZettel)

		default:
			var zn *ast.ZettelNode
			var em func(value string) ast.InlineSlice
			if q.Has(api.QueryKeyParseOnly) {
				zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
				em = parser.ParseMetadata
			} else {
				zn, err = evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
				em = func(value string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, value)
				}
			}
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
			a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part)
		}
	})
}

func (a *API) writePlainData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var contentType string
	var err error

	z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
	if err != nil {
		a.reportUsecaseError(w, err)
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
		return
	}
	if err = writeBuffer(w, &buf, contentType); err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("Write Plain data")
	}
}

func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)
		return
	}
	var obj sx.Object
	switch part {







|







113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
		return
	}
	if err = writeBuffer(w, &buf, contentType); err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("Write Plain data")
	}
}

func (a *API) writeSzData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)
		return
	}
	var obj sx.Object
	switch part {

Changes to web/adapter/api/login.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

package api

import (
	"net/http"
	"time"

	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// 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() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Login/free")
			}
			return
		}
		var token []byte







|







|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

package api

import (
	"net/http"
	"time"

	"t73f.de/r/sx"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Login/free")
			}
			return
		}
		var token []byte
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Login")
		}
	}
}

func retrieveIdentCred(r *http.Request) (string, string) {
	if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok {
		return ident, cred
	}
	if ident, cred, ok := r.BasicAuth(); ok {
		return ident, cred
	}
	return "", ""
}

// 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()
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Refresh/free")
			}
			return
		}







|













|
|







47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Login")
		}
	})
}

func retrieveIdentCred(r *http.Request) (string, string) {
	if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok {
		return ident, cred
	}
	if ident, cred, ok := r.BasicAuth(); ok {
		return ident, cred
	}
	return "", ""
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Refresh/free")
			}
			return
		}
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Write renewed token")
		}
	}
}

func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
	return a.writeObject(w, id.Invalid, sx.MakeList(
		sx.String("Bearer"),
		sx.String(token),
		sx.Int64(int64(lifetime/time.Second)),
	))
}







|




|
|



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Write renewed token")
		}
	})
}

func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
	return a.writeObject(w, id.Invalid, sx.MakeList(
		sx.MakeString("Bearer"),
		sx.MakeString(token),
		sx.Int64(int64(lifetime/time.Second)),
	))
}

Changes to web/adapter/api/query.go.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37





38
39
40
41
42
43
44
45
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc {





	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		urlQuery := r.URL.Query()
		if a.handleTagZettel(w, r, tagZettel, urlQuery) || a.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}

		sq := adapter.GetQuery(urlQuery)







|
|
|









|
>
>
>
>
>
|







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,
	reIndex *usecase.ReIndex,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		urlQuery := r.URL.Query()
		if a.handleTagZettel(w, r, tagZettel, urlQuery) || a.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}

		sq := adapter.GetQuery(urlQuery)
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
			a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}

		if err = writeBuffer(w, &buf, contentType); err != nil {
			a.log.Error().Err(err).Msg("write result buffer")
		}
	}
}
func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error {
	min, max := -1, -1
	if len(actions) > 0 {
		acts := make([]string, 0, len(actions))
		for _, act := range actions {
			if strings.HasPrefix(act, api.MinAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					min = num
					continue
				}
			}
			if strings.HasPrefix(act, api.MaxAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					max = num
					continue
				}
			}
			acts = append(acts, act)
		}
		for _, act := range acts {
			if act == api.KeysAction {
				return encodeKeysArrangement(w, enc, ml, act)
			}
			switch key := strings.ToLower(act); meta.Type(key) {
			case meta.TypeWord, meta.TypeTagSet:
				return encodeMetaKeyArrangement(w, enc, ml, key, min, max)
			}
		}
	}
	return enc.writeMetaList(w, ml)
}

func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error {
	arr := make(meta.Arrangement, 128)
	for _, m := range ml {
		for k := range m.Map() {
			arr[k] = append(arr[k], m)
		}
	}
	return enc.writeArrangement(w, act, arr)
}

func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error {
	arr0 := meta.CreateArrangement(ml, key)
	arr := make(meta.Arrangement, len(arr0))
	for k0, ml0 := range arr0 {
		if len(ml0) < min || (max > 0 && len(ml0) > max) {
			continue
		}
		arr[k0] = ml0
	}
	return enc.writeArrangement(w, key, arr)
}








|


|





|





|











|
















|



|







97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
			a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}

		if err = writeBuffer(w, &buf, contentType); err != nil {
			a.log.Error().Err(err).Msg("write result buffer")
		}
	})
}
func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error {
	minVal, maxVal := -1, -1
	if len(actions) > 0 {
		acts := make([]string, 0, len(actions))
		for _, act := range actions {
			if strings.HasPrefix(act, api.MinAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					minVal = num
					continue
				}
			}
			if strings.HasPrefix(act, api.MaxAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					maxVal = num
					continue
				}
			}
			acts = append(acts, act)
		}
		for _, act := range acts {
			if act == api.KeysAction {
				return encodeKeysArrangement(w, enc, ml, act)
			}
			switch key := strings.ToLower(act); meta.Type(key) {
			case meta.TypeWord, meta.TypeTagSet:
				return encodeMetaKeyArrangement(w, enc, ml, key, minVal, maxVal)
			}
		}
	}
	return enc.writeMetaList(w, ml)
}

func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error {
	arr := make(meta.Arrangement, 128)
	for _, m := range ml {
		for k := range m.Map() {
			arr[k] = append(arr[k], m)
		}
	}
	return enc.writeArrangement(w, act, arr)
}

func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, minVal, maxVal int) error {
	arr0 := meta.CreateArrangement(ml, key)
	arr := make(meta.Arrangement, len(arr0))
	for k0, ml0 := range arr0 {
		if len(ml0) < minVal || (maxVal > 0 && len(ml0) > maxVal) {
			continue
		}
		arr[k0] = ml0
	}
	return enc.writeArrangement(w, key, arr)
}

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
		})
		msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel)
		result[i+1] = msz
	}

	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("meta-list"),
		sx.MakeList(sx.MakeSymbol("query"), sx.String(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.String(dze.sq.Human())),
		sx.MakeList(result...),
	))
	return err
}
func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
	result := sx.Nil()
	for aggKey, metaList := range arr {
		sxMeta := sx.Nil()
		for i := len(metaList) - 1; i >= 0; i-- {
			sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
		}
		sxMeta = sxMeta.Cons(sx.String(aggKey))
		result = result.Cons(sxMeta)
	}
	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("aggregate"),
		sx.String(act),
		sx.MakeList(sx.MakeSymbol("query"), sx.String(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.String(dze.sq.Human())),
		result.Cons(sx.SymbolList),
	))
	return err
}

func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)







|
|











|




|
|
|







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
		})
		msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel)
		result[i+1] = msz
	}

	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("meta-list"),
		sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
		sx.MakeList(result...),
	))
	return err
}
func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
	result := sx.Nil()
	for aggKey, metaList := range arr {
		sxMeta := sx.Nil()
		for i := len(metaList) - 1; i >= 0; i-- {
			sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
		}
		sxMeta = sxMeta.Cons(sx.MakeString(aggKey))
		result = result.Cons(sxMeta)
	}
	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("aggregate"),
		sx.MakeString(act),
		sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
		result.Cons(sx.SymbolList),
	))
	return err
}

func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)

Deleted web/adapter/api/rename_zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package api

import (
	"net/http"
	"net/url"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeRenameZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		newZid, found := getDestinationZid(r)
		if !found {
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		if err = renameZettel.Run(r.Context(), zid, newZid); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

func getDestinationZid(r *http.Request) (id.Zid, bool) {
	if values, ok := r.Header[api.HeaderDestination]; ok {
		for _, value := range values {
			if zid, ok2 := getZidFromURL(value); ok2 {
				return zid, true
			}
		}
	}
	return id.Invalid, false
}

func getZidFromURL(val string) (id.Zid, bool) {
	u, err := url.Parse(val)
	if err != nil {
		return id.Invalid, false
	}
	if len(u.Path) < len(api.ZidVersion) {
		return id.Invalid, false
	}
	zid, err := id.Parse(u.Path[len(u.Path)-len(api.ZidVersion):])
	if err != nil {
		return id.Invalid, false
	}
	return zid, true
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































































Changes to web/adapter/api/request.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package api

import (
	"io"
	"net/http"
	"net/url"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// getEncoding returns the data encoding selected by the caller.
func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) {







|
|
|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package api

import (
	"io"
	"net/http"
	"net/url"

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// getEncoding returns the data encoding selected by the caller.
func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) {

Changes to web/adapter/api/response.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package api

import (
	"bytes"
	"net/http"

	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
)

func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, obj); err != nil {







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package api

import (
	"bytes"
	"net/http"

	"t73f.de/r/sx"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
)

func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, obj); err != nil {

Changes to web/adapter/api/update_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
//-----------------------------------------------------------------------------

package api

import (
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()







|







|
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//-----------------------------------------------------------------------------

package api

import (
	"net/http"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()
49
50
51
52
53
54
55
56
57
			return
		}
		if err = updateZettel.Run(r.Context(), zettel, true); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}







|

49
50
51
52
53
54
55
56
57
			return
		}
		if err = updateZettel.Run(r.Context(), zettel, true); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})
}

Changes to web/adapter/request.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
)

// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
)

// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()

Changes to web/adapter/response.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"errors"
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// WriteData emits the given data to the response writer.
func WriteData(w http.ResponseWriter, data []byte, contentType string) error {
	if len(data) == 0 {







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"errors"
	"fmt"
	"net/http"
	"strings"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// WriteData emits the given data to the response writer.
func WriteData(w http.ResponseWriter, data []byte, contentType string) error {
	if len(data) == 0 {
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
		msg := ena.Error()
		return http.StatusForbidden, strings.ToUpper(msg[:1]) + msg[1:]
	}
	var eiz box.ErrInvalidZid
	if errors.As(err, &eiz) {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid)
	}
	var ezin usecase.ErrZidInUse
	if errors.As(err, &ezin) {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", ezin.Zid)
	}
	var etznf usecase.ErrTagZettelNotFound
	if errors.As(err, &etznf) {
		return http.StatusNotFound, "Tag zettel not found: " + etznf.Tag
	}
	var erznf usecase.ErrRoleZettelNotFound
	if errors.As(err, &erznf) {
		return http.StatusNotFound, "Role zettel not found: " + erznf.Role







<
<
<
<







66
67
68
69
70
71
72




73
74
75
76
77
78
79
		msg := ena.Error()
		return http.StatusForbidden, strings.ToUpper(msg[:1]) + msg[1:]
	}
	var eiz box.ErrInvalidZid
	if errors.As(err, &eiz) {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid)
	}




	var etznf usecase.ErrTagZettelNotFound
	if errors.As(err, &etznf) {
		return http.StatusNotFound, "Tag zettel not found: " + etznf.Tag
	}
	var erznf usecase.ErrRoleZettelNotFound
	if errors.As(err, &erznf) {
		return http.StatusNotFound, "Role zettel not found: " + erznf.Role

Changes to web/adapter/webui/const.go.

15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

// WebUI related constants.

const queryKeyAction = "_action"

// Values for queryKeyAction
const (
	valueActionChild   = "child"
	valueActionCopy    = "copy"
	valueActionFolge   = "folge"
	valueActionNew     = "new"

	valueActionVersion = "version"
)

// Enumeration for queryKeyAction
type createAction uint8

const (
	actionChild createAction = iota
	actionCopy
	actionFolge
	actionNew
	actionVersion
)

var createActionMap = map[string]createAction{
	valueActionChild:   actionChild,
	valueActionCopy:    actionCopy,
	valueActionFolge:   actionFolge,
	valueActionNew:     actionNew,
	valueActionVersion: actionVersion,
}

func getCreateAction(s string) createAction {







<



>







|
|
|
|




|







15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

// WebUI related constants.

const queryKeyAction = "_action"

// Values for queryKeyAction
const (

	valueActionCopy    = "copy"
	valueActionFolge   = "folge"
	valueActionNew     = "new"
	valueActionSequel  = "sequel"
	valueActionVersion = "version"
)

// Enumeration for queryKeyAction
type createAction uint8

const (
	actionCopy createAction = iota
	actionFolge
	actionNew
	actionSequel
	actionVersion
)

var createActionMap = map[string]createAction{
	valueActionSequel:  actionSequel,
	valueActionCopy:    actionCopy,
	valueActionFolge:   actionFolge,
	valueActionNew:     actionNew,
	valueActionVersion: actionVersion,
}

func getCreateAction(s string) createAction {

Changes to web/adapter/webui/create_zettel.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

40


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68


69
70
71
72
73
74
75
76
77
78
79

import (
	"bytes"
	"context"
	"net/http"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel, createZettel *usecase.CreateZettel,

	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {


	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		op := getCreateAction(q.Get(queryKeyAction))
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}
		origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid})
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {
		case actionChild:
			wui.renderZettelForm(ctx, w, createZettel.PrepareChild(origZettel), "Child Zettel", "", roleData, syntaxData)
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle))
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)


		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		}
	}
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
	roleData := dataListFromArrangement(ucListRoles.Run(ctx))
	syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx))
	return roleData, syntaxData
}







|
|















|
>
|
>
>
|

















<
<








>
>



|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

import (
	"bytes"
	"context"
	"net/http"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel,
	createZettel *usecase.CreateZettel,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		op := getCreateAction(q.Get(queryKeyAction))
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}
		origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid})
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {


		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle))
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)
		case actionSequel:
			wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		}
	})
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
	roleData := dataListFromArrangement(ucListRoles.Run(ctx))
	syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx))
	return roleData, syntaxData
}
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
	var sb strings.Builder
	for _, p := range m.PairsRest() {
		sb.WriteString(p.Key)
		sb.WriteString(": ")
		sb.WriteString(p.Value)
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
	rb.bindString("heading", sx.String(title))
	rb.bindString("form-action-url", sx.String(formActionURL))
	rb.bindString("role-data", makeStringList(roleData))
	rb.bindString("syntax-data", makeStringList(syntaxData))
	rb.bindString("meta", sx.String(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.String(ztl.Content.AsString()))
	}
	wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content)
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}
}

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		reEdit, zettel, err := parseZettelForm(r, id.Invalid)
		if err == errMissingContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
			return
		}
		if err != nil {







|
|
|


|

|












|
|







105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
	var sb strings.Builder
	for _, p := range m.PairsRest() {
		sb.WriteString(p.Key)
		sb.WriteString(": ")
		sb.WriteString(p.Value)
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnv(ctx, "form", wui.getUserLang(ctx), title, user)
	rb.bindString("heading", sx.MakeString(title))
	rb.bindString("form-action-url", sx.MakeString(formActionURL))
	rb.bindString("role-data", makeStringList(roleData))
	rb.bindString("syntax-data", makeStringList(syntaxData))
	rb.bindString("meta", sx.MakeString(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.MakeString(ztl.Content.AsString()))
	}
	wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content)
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}
}

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		reEdit, zettel, err := parseZettelForm(r, id.Invalid)
		if err == errMissingContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
			return
		}
		if err != nil {
147
148
149
150
151
152
153
154
155
156
157
158
159
160

161
162

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
			return
		}
		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
		}
	}
}

// MakeGetZettelFromListHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeGetZettelFromListHandler(
	queryMeta *usecase.Query, evaluate *usecase.Evaluate,

	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {


	return func(w http.ResponseWriter, r *http.Request) {
		q := adapter.GetQuery(r.URL.Query())
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig)
		bns := evaluate.RunBlockNode(ctx, entries)
		enc := zmkenc.Create()
		var zmkContent bytes.Buffer
		_, err = enc.WriteBlocks(&zmkContent, &bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		m := meta.New(id.Invalid)
		m.Set(api.KeyTitle, q.Human())
		m.Set(api.KeySyntax, api.ValueSyntaxZmk)
		if qval := q.String(); qval != "" {
			m.Set(api.KeyQuery, qval)
		}
		zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())}
		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData)
	}
}







|





|
>
|
|
>
|







|


















|

150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
			return
		}
		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
		}
	})
}

// MakeGetZettelFromListHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeGetZettelFromListHandler(
	queryMeta *usecase.Query,
	evaluate *usecase.Evaluate,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		q := adapter.GetQuery(r.URL.Query())
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq)
		bns := evaluate.RunBlockNode(ctx, entries)
		enc := zmkenc.Create()
		var zmkContent bytes.Buffer
		_, err = enc.WriteBlocks(&zmkContent, &bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		m := meta.New(id.Invalid)
		m.Set(api.KeyTitle, q.Human())
		m.Set(api.KeySyntax, api.ValueSyntaxZmk)
		if qval := q.String(); qval != "" {
			m.Set(api.KeyQuery, qval)
		}
		zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())}
		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData)
	})
}

Changes to web/adapter/webui/delete_zettel.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
//-----------------------------------------------------------------------------

package webui

import (
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc {



	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "delete",
			wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.String(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???")))
			rb.bindString("incoming", nil)
		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		}
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)

		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair {
	zidMap := make(strfun.Set)
	addListValues(zidMap, m, api.KeyBackward)
	for _, kd := range meta.GetSortedKeyDescriptions() {
		inverseKey := kd.Inverse







|
|
|










|
>
>
>
|

















<
|

|















|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

54
55
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 webui

import (
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/box"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(
	getZettel usecase.GetZettel,
	getAllZettel usecase.GetAllZettel,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(

			ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.MakeString(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???")))
			rb.bindString("incoming", nil)
		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		}
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)

		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	})
}

func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair {
	zidMap := make(strfun.Set)
	addListValues(zidMap, m, api.KeyBackward)
	for _, kd := range meta.GetSortedKeyDescriptions() {
		inverseKey := kd.Inverse
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
		for _, val := range values {
			zidMap.Set(val)
		}
	}
}

// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}







|
|













|

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
		for _, val := range values {
			zidMap.Set(val)
		}
	}
}

// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	})
}

Changes to web/adapter/webui/edit_zettel.go.

20
21
22
23
24
25
26
27




28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {




	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData)
	}
}

// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}







|
>
>
>
>
|
















|




|
|







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(
	getZettel usecase.GetZettel,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData)
	})
}

// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}
74
75
76
77
78
79
80
81
82
		}

		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID()))
		}
	}
}







|

78
79
80
81
82
83
84
85
86
		}

		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID()))
		}
	})
}

Changes to web/adapter/webui/favicon.go.

18
19
20
21
22
23
24

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
	"net/http"
	"os"
	"path/filepath"

	"zettelstore.de/z/web/adapter"
)


func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			wui.log.Debug().Err(err).Msg("Favicon not found")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		defer f.Close()

		data, err := io.ReadAll(f)
		if err != nil {
			wui.log.Error().Err(err).Msg("Unable to read favicon data")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}

		if err = adapter.WriteData(w, data, ""); err != nil {
			wui.log.Error().Err(err).Msg("Write favicon")
		}
	}
}







>
|
|



















|

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
	"net/http"
	"os"
	"path/filepath"

	"zettelstore.de/z/web/adapter"
)

// MakeFaviconHandler creates a HTTP handler to retrieve the favicon.
func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			wui.log.Debug().Err(err).Msg("Favicon not found")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		defer f.Close()

		data, err := io.ReadAll(f)
		if err != nil {
			wui.log.Error().Err(err).Msg("Unable to read favicon data")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}

		if err = adapter.WriteData(w, data, ""); err != nil {
			wui.log.Error().Err(err).Msg("Write favicon")
		}
	})
}

Changes to web/adapter/webui/forms.go.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"errors"
	"io"
	"net/http"
	"regexp"
	"strings"
	"unicode"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)







|
|







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"errors"
	"io"
	"net/http"
	"regexp"
	"strings"
	"unicode"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

Changes to web/adapter/webui/get_info.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//-----------------------------------------------------------------------------

package webui

import (
	"context"
	"net/http"
	"sort"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
)

// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(
	ucParseZettel usecase.ParseZettel,
	ucEvaluate *usecase.Evaluate,
	ucGetZettel usecase.GetZettel,
	ucGetAllMeta usecase.GetAllZettel,
	ucQuery *usecase.Query,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()

		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
		getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
		evalMeta := func(val string) ast.InlineSlice {
			return ucEvaluate.RunMetadata(ctx, val)
		}
		pairs := zn.Meta.ComputedPairs()
		metadata := sx.Nil()
		for i := len(pairs) - 1; i >= 0; i-- {
			key := pairs[i].Key
			sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
			metadata = metadata.Cons(sx.Cons(sx.String(key), sxval))
		}

		summary := collect.References(zn)
		locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = title
		}
		unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)
		bns := ucEvaluate.RunBlockNode(ctx, entries)
		unlinkedContent, _, err := enc.BlocksSxn(&bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		encTexts := encodingTexts()
		shadowLinks := getShadowLinks(ctx, zid, ucGetAllMeta)

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
		rb.bindString("metadata", metadata)
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.String(phrase))
		rb.bindString("query-key-phrase", sx.String(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) {
	for i := len(links) - 1; i >= 0; i-- {
		ref := links[i]
		if ref.State == ast.RefStateSelf || ref.IsZettel() {
			continue

		}
		if ref.State == ast.RefStateQuery {
			queries = queries.Cons(
				sx.Cons(
					sx.String(ref.Value),
					sx.String(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))
			continue
		}
		if ref.IsExternal() {
			extLinks = extLinks.Cons(sx.String(ref.String()))
			continue
		}
		locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.String(ref.String())))
	}
	return locLinks, queries, extLinks
}

func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
	var sb strings.Builder
	sb.Write(zid.Bytes())







|


|
|


















|

|
|
















|









|
















|







|


|





|
|












|





|
|
>
|
|


|
|
<
|
|
|
<

<







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

135
136
137

138

139
140
141
142
143
144
145
//-----------------------------------------------------------------------------

package webui

import (
	"context"
	"net/http"
	"slices"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
)

// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(
	ucParseZettel usecase.ParseZettel,
	ucEvaluate *usecase.Evaluate,
	ucGetZettel usecase.GetZettel,
	ucGetAllZettel usecase.GetAllZettel,
	ucQuery *usecase.Query,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()

		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, api.KeyLang))
		getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
		evalMeta := func(val string) ast.InlineSlice {
			return ucEvaluate.RunMetadata(ctx, val)
		}
		pairs := zn.Meta.ComputedPairs()
		metadata := sx.Nil()
		for i := len(pairs) - 1; i >= 0; i-- {
			key := pairs[i].Key
			sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
			metadata = metadata.Cons(sx.Cons(sx.MakeString(key), sxval))
		}

		summary := collect.References(zn)
		locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = title
		}
		unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta)
		bns := ucEvaluate.RunBlockNode(ctx, entries)
		unlinkedContent, _, err := enc.BlocksSxn(&bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		encTexts := encodingTexts()
		shadowLinks := getShadowLinks(ctx, zid, ucGetAllZettel)

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "info", wui.getUserLang(ctx), title, user)
		rb.bindString("metadata", metadata)
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.MakeString(phrase))
		rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	})
}

func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) {
	for i := len(links) - 1; i >= 0; i-- {
		ref := links[i]
		switch ref.State {
		case ast.RefStateHosted, ast.RefStateBased: // Local
			locLinks = locLinks.Cons(sx.MakeString(ref.String()))

		case ast.RefStateQuery:
			queries = queries.Cons(
				sx.Cons(
					sx.MakeString(ref.Value),
					sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))


		case ast.RefStateExternal:
			extLinks = extLinks.Cons(sx.MakeString(ref.String()))

		}

	}
	return locLinks, queries, extLinks
}

func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
	var sb strings.Builder
	sb.Write(zid.Bytes())
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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

func encodingTexts() []string {
	encodings := encoder.GetEncodings()
	encTexts := make([]string, 0, len(encodings))
	for _, f := range encodings {
		encTexts = append(encTexts, f.String())
	}
	sort.Strings(encTexts)
	return encTexts
}

var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent}

func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair {
	matrix := sx.Nil()
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
	for ip := len(apiParts) - 1; ip >= 0; ip-- {
		part := apiParts[ip]
		row := sx.Nil()
		for je := len(encTexts) - 1; je >= 0; je-- {
			enc := encTexts[je]
			if parseOnly {
				u.AppendKVQuery(api.QueryKeyParseOnly, "")
			}
			u.AppendKVQuery(api.QueryKeyPart, part)
			u.AppendKVQuery(api.QueryKeyEncoding, enc)
			row = row.Cons(sx.Cons(sx.String(enc), sx.String(u.String())))
			u.ClearQuery()
		}
		matrix = matrix.Cons(sx.Cons(sx.String(part), row))
	}
	return matrix
}

func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair {
	matrix := wui.infoAPIMatrix(zid, true, encTexts)
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())

	for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() {
		line, isLine := sx.GetPair(row.Car())
		if !isLine || line == nil {
			continue
		}
		last := line.LastPair()
		part := apiParts[i]
		u.AppendKVQuery(api.QueryKeyPart, part)
		last = last.AppendBang(sx.Cons(sx.String("plain"), sx.String(u.String())))
		u.ClearQuery()
		if i < 2 {
			u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
			u.AppendKVQuery(api.QueryKeyPart, part)
			last.AppendBang(sx.Cons(sx.String("data"), sx.String(u.String())))
			u.ClearQuery()
		}
		i++
	}
	return matrix
}

func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair {
	result := sx.Nil()
	if zl, err := getAllZettel.Run(ctx, zid); err == nil {
		for i := len(zl) - 1; i >= 1; i-- {
			if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
				result = result.Cons(sx.String(boxNo))
			}
		}
	}
	return result
}







|


















|


|
















|




|












|





160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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

func encodingTexts() []string {
	encodings := encoder.GetEncodings()
	encTexts := make([]string, 0, len(encodings))
	for _, f := range encodings {
		encTexts = append(encTexts, f.String())
	}
	slices.Sort(encTexts)
	return encTexts
}

var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent}

func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair {
	matrix := sx.Nil()
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
	for ip := len(apiParts) - 1; ip >= 0; ip-- {
		part := apiParts[ip]
		row := sx.Nil()
		for je := len(encTexts) - 1; je >= 0; je-- {
			enc := encTexts[je]
			if parseOnly {
				u.AppendKVQuery(api.QueryKeyParseOnly, "")
			}
			u.AppendKVQuery(api.QueryKeyPart, part)
			u.AppendKVQuery(api.QueryKeyEncoding, enc)
			row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String())))
			u.ClearQuery()
		}
		matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row))
	}
	return matrix
}

func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair {
	matrix := wui.infoAPIMatrix(zid, true, encTexts)
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())

	for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() {
		line, isLine := sx.GetPair(row.Car())
		if !isLine || line == nil {
			continue
		}
		last := line.LastPair()
		part := apiParts[i]
		u.AppendKVQuery(api.QueryKeyPart, part)
		last = last.AppendBang(sx.Cons(sx.MakeString("plain"), sx.MakeString(u.String())))
		u.ClearQuery()
		if i < 2 {
			u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
			u.AppendKVQuery(api.QueryKeyPart, part)
			last.AppendBang(sx.Cons(sx.MakeString("data"), sx.MakeString(u.String())))
			u.ClearQuery()
		}
		i++
	}
	return matrix
}

func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair {
	result := sx.Nil()
	if zl, err := getAllZettel.Run(ctx, zid); err == nil {
		for i := len(zl) - 1; i >= 1; i-- {
			if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
				result = result.Cons(sx.MakeString(boxNo))
			}
		}
	}
	return result
}

Changes to web/adapter/webui/get_zettel.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package webui

import (
	"context"
	"net/http"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc {



	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		q := r.URL.Query()
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}


		enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
		metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		content, endnotes, err := enc.BlocksSxn(&zn.Ast)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := server.GetUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user)
		rb.bindSymbol(symMetaHeader, metaObj)
		rb.bindString("heading", sx.String(title))
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			rb.bindString("role-url", sx.String(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
		}
		if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" {
			rb.bindString("folge-role-url", sx.String(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+folgeRole).String()))
		}
		rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
		rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
		rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
		rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle))
		rb.bindString("urls", metaURLAssoc(zn.InhMeta))
		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, api.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "back", zn.InhMeta, api.KeyBack, config.KeyShowBackLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, api.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle)
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			for _, part := range []string{"meta", "actions", "heading"} {
				rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part)
			}
		}
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	if values, ok := m.GetList(key); ok {
		return wui.transformIdentifierSet(values, getTextTitle)
	}
	return nil
}

func metaURLAssoc(m *meta.Meta) *sx.Pair {
	var result sx.ListBuilder
	for _, p := range m.PairsRest() {
		if key := p.Key; strings.HasSuffix(key, meta.SuffixKeyURL) {
			if val := p.Value; val != "" {
				result.Add(sx.Cons(sx.String(capitalizeMetaKey(key)), sx.String(val)))
			}
		}
	}
	return result.List()
}

func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) {
	varLinks := varPrefix + "-links"
	var symOpen *sx.Symbol
	switch wui.rtConfig.Get(ctx, m, configKey) {
	case "false":
		rb.bindString(varLinks, sx.Nil())
		return
	case "close":
	default:
		symOpen = shtml.SymAttrOpen
	}







|
|
|










|
>
>
>
|















>
|











|

|

|


|




|




|
















|














|









|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package webui

import (
	"context"
	"net/http"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(
	evaluate *usecase.Evaluate,
	getZettel usecase.GetZettel,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		q := r.URL.Query()
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		zettelLang := wui.getConfig(ctx, zn.InhMeta, api.KeyLang)
		enc := wui.getSimpleHTMLEncoder(zettelLang)
		metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		content, endnotes, err := enc.BlocksSxn(&zn.Ast)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := server.GetUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnv(ctx, "zettel", zettelLang, title, user)
		rb.bindSymbol(symMetaHeader, metaObj)
		rb.bindString("heading", sx.MakeString(title))
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			rb.bindString("role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
		}
		if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" {
			rb.bindString("folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+folgeRole).String()))
		}
		rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
		rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
		rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
		rb.bindString("prequel-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrequel, getTextTitle))
		rb.bindString("urls", metaURLAssoc(zn.InhMeta))
		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "sequel", zn.InhMeta, api.KeySequel, config.KeyShowSequelLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "back", zn.InhMeta, api.KeyBack, config.KeyShowBackLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, api.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle)
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			for _, part := range []string{"meta", "actions", "heading"} {
				rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part)
			}
		}
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	})
}

func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	if values, ok := m.GetList(key); ok {
		return wui.transformIdentifierSet(values, getTextTitle)
	}
	return nil
}

func metaURLAssoc(m *meta.Meta) *sx.Pair {
	var result sx.ListBuilder
	for _, p := range m.PairsRest() {
		if key := p.Key; strings.HasSuffix(key, meta.SuffixKeyURL) {
			if val := p.Value; val != "" {
				result.Add(sx.Cons(sx.MakeString(capitalizeMetaKey(key)), sx.MakeString(val)))
			}
		}
	}
	return result.List()
}

func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) {
	varLinks := varPrefix + "-links"
	var symOpen *sx.Symbol
	switch wui.getConfig(ctx, m, configKey) {
	case "false":
		rb.bindString(varLinks, sx.Nil())
		return
	case "close":
	default:
		symOpen = shtml.SymAttrOpen
	}
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
	for i := len(values) - 1; i >= 0; i-- {
		val := values[i]
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTextTitle(zid); found > 0 {
			url := sx.String(wui.NewURLBuilder('h').SetZid(zid.ZettelID()).String())
			if title == "" {
				lst = lst.Cons(sx.Cons(sx.String(val), url))
			} else {
				lst = lst.Cons(sx.Cons(sx.String(title), url))
			}
		}
	}
	return lst
}







|

|

|





151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
	for i := len(values) - 1; i >= 0; i-- {
		val := values[i]
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTextTitle(zid); found > 0 {
			url := sx.MakeString(wui.NewURLBuilder('h').SetZid(zid.ZettelID()).String())
			if title == "" {
				lst = lst.Cons(sx.Cons(sx.MakeString(val), url))
			} else {
				lst = lst.Cons(sx.Cons(sx.MakeString(title), url))
			}
		}
	}
	return lst
}

Changes to web/adapter/webui/goaction.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import (
	"net/http"

	"zettelstore.de/z/usecase"
)

// MakeGetGoActionHandler creates a new HTTP handler to execute certain commands.
func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		// Currently, command "refresh" is the only command to be executed.
		err := ucRefresh.Run(ctx)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}







|
|









|

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import (
	"net/http"

	"zettelstore.de/z/usecase"
)

// MakeGetGoActionHandler creates a new HTTP handler to execute certain commands.
func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		// Currently, command "refresh" is the only command to be executed.
		err := ucRefresh.Run(ctx)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	})
}

Changes to web/adapter/webui/home.go.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
	"zettelstore.de/z/config"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

type getRootStore interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if p := r.URL.Path; p != "/" {
			wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p})
			return
		}
		homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel))
		apiHomeZid := homeZid.ZettelID()
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetZettel(ctx, homeZid); err == nil {
				wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
				return
			}
			homeZid = id.DefaultHomeZid
		}
		_, err := s.GetZettel(ctx, homeZid)
		if err == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
			return
		}
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('i'))
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('h'))
	}
}







|




|
|





|


















|

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
	"zettelstore.de/z/config"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

type getRootPort interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func (wui *WebUI) MakeGetRootHandler(s getRootPort) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if p := r.URL.Path; p != "/" {
			wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p})
			return
		}
		homeZid, _ := id.Parse(wui.getConfig(ctx, nil, config.KeyHomeZettel))
		apiHomeZid := homeZid.ZettelID()
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetZettel(ctx, homeZid); err == nil {
				wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
				return
			}
			homeZid = id.DefaultHomeZid
		}
		_, err := s.GetZettel(ctx, homeZid)
		if err == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
			return
		}
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('i'))
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('h'))
	})
}

Changes to web/adapter/webui/htmlgen.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

package webui

import (
	"net/url"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/client.fossil/sz"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)








|
|
|
|
|
|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

package webui

import (
	"net/url"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/maps"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		zid, fragment, hasFragment := strings.Cut(string(href), "#")
		u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	}

	rebind(th, sz.SymLinkZettel, linkZettel)
	rebind(th, sz.SymLinkFound, linkZettel)
	rebind(th, sz.SymLinkBased, func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		u := builder.NewURLBuilder('/').SetRawLocal(string(href))
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkQuery, func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		ur, err := url.Parse(string(href))
		if err != nil {
			return obj
		}
		q := ur.Query().Get(api.QueryKeyQuery)
		if q == "" {
			return obj
		}
		u := builder.NewURLBuilder('h').AppendQuery(q)



		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrClass, sx.String("external"))).
			Cons(sx.Cons(shtml.SymAttrTarget, sx.String("_blank"))).
			Cons(sx.Cons(shtml.SymAttrRel, sx.String("noopener noreferrer")))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymEmbed, func(obj sx.Object) sx.Object {
		pair, isPair := sx.GetPair(obj)
		if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) {
			return obj
		}
		attr, isPair := sx.GetPair(pair.Tail().Car())
		if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) {
			return obj
		}
		srcP := attr.Tail().Assoc(shtml.SymAttrSrc)
		if srcP == nil {
			return obj
		}
		src, isString := sx.GetString(srcP.Cdr())
		if !isString {
			return obj
		}
		zid := api.ZettelID(src)
		if !zid.IsValid() {
			return obj
		}
		u := builder.NewURLBuilder('z').SetZid(zid)
		imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.String(u.String()))).Cons(sxhtml.SymAttr)
		return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG)
	})

	return &htmlGenerator{
		tx:   szenc.NewTransformer(),
		th:   th,
		lang: lang,







|




|


















|
|















|



|
|


|
>
>
>
|



|



|
|
|
|


















|




|







73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		zid, fragment, hasFragment := strings.Cut(href.GetValue(), "#")
		u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	}

	rebind(th, sz.SymLinkZettel, linkZettel)
	rebind(th, sz.SymLinkFound, linkZettel)
	rebind(th, sz.SymLinkBased, func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		u := builder.NewURLBuilder('/')
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()+href.GetValue()[1:])))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkQuery, func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		ur, err := url.Parse(href.GetValue())
		if err != nil {
			return obj
		}
		urlQuery := ur.Query()
		if !urlQuery.Has(api.QueryKeyQuery) {
			return obj
		}
		u := builder.NewURLBuilder('h')
		if q := urlQuery.Get(api.QueryKeyQuery); q != "" {
			u = u.AppendQuery(q)
		}
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object {
		attr, _, rest := findA(obj)
		if attr == nil {
			return obj
		}
		a := sz.GetAttributes(attr)
		a = a.Set("target", "_blank")
		a = a.Add("rel", "external").Add("rel", "noreferrer")
		return rest.Cons(shtml.EvaluateAttrbute(a)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymEmbed, func(obj sx.Object) sx.Object {
		pair, isPair := sx.GetPair(obj)
		if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) {
			return obj
		}
		attr, isPair := sx.GetPair(pair.Tail().Car())
		if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) {
			return obj
		}
		srcP := attr.Tail().Assoc(shtml.SymAttrSrc)
		if srcP == nil {
			return obj
		}
		src, isString := sx.GetString(srcP.Cdr())
		if !isString {
			return obj
		}
		zid := api.ZettelID(src.GetValue())
		if !zid.IsValid() {
			return obj
		}
		u := builder.NewURLBuilder('z').SetZid(zid)
		imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.MakeString(u.String()))).Cons(sxhtml.SymAttr)
		return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG)
	})

	return &htmlGenerator{
		tx:   szenc.NewTransformer(),
		th:   th,
		lang: lang,
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
	}
	sx := g.tx.GetSz(bs)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil, nil, err
	}
	return sh, g.th.Endnotes(&env), nil
}

// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair {
	if is == nil || len(*is) == 0 {
		return nil
	}







|







274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
	}
	sx := g.tx.GetSz(bs)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil, nil, err
	}
	return sh, shtml.Endnotes(&env), nil
}

// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair {
	if is == nil || len(*is) == 0 {
		return nil
	}

Changes to web/adapter/webui/htmlmeta.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

package webui

import (
	"context"
	"errors"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) writeHTMLMetaValue(
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
) sx.Object {
	switch kt := meta.Type(key); kt {
	case meta.TypeCredential:
		return sx.String(value)
	case meta.TypeEmpty:
		return sx.String(value)
	case meta.TypeID:
		return wui.transformIdentifier(value, getTextTitle)
	case meta.TypeIDSet:
		return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
	case meta.TypeNumber:
		return wui.transformKeyValueText(key, value, value)
	case meta.TypeString:
		return sx.String(value)
	case meta.TypeTagSet:
		return wui.transformTagSet(key, meta.ListFromValue(value))
	case meta.TypeTimestamp:
		if ts, ok := meta.TimeValue(value); ok {
			return sx.MakeList(
				sx.MakeSymbol("time"),
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(sx.MakeSymbol("datetime"), sx.String(ts.Format("2006-01-02T15:04:05"))),
				),
				sx.MakeList(sxhtml.SymNoEscape, sx.String(ts.Format("2006-01-02&nbsp;15:04:05"))),
			)
		}
		return sx.Nil()
	case meta.TypeURL:
		return wui.url2html(sx.String(value))
	case meta.TypeWord:
		return wui.transformKeyValueText(key, value, value)
	case meta.TypeZettelmarkup:
		return wui.transformZmkMetadata(value, evalMetadata, gen)
	default:
		return sx.MakeList(shtml.SymSTRONG, sx.String("Unhandled type: "), sx.String(kt.Name))
	}
}

func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
	text := sx.String(val)
	zid, err := id.Parse(val)
	if err != nil {
		return text
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(zid.ZettelID())
		attrs := sx.Nil()
		if title != "" {
			attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.String(title)))
		}
		attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.String(ub.String()))).Cons(sxhtml.SymAttr)
		return sx.Nil().Cons(sx.String(zid.String())).Cons(attrs).Cons(shtml.SymA)
	case found == 0:
		return sx.MakeList(sx.MakeSymbol("s"), text)
	default: // case found < 0:
		return text
	}
}

func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
	if len(vals) == 0 {
		return nil
	}
	const space = sx.String(" ")
	text := make(sx.Vector, 0, 2*len(vals))
	for _, val := range vals {
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
	}
	return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
}

func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
	if len(tags) == 0 {
		return nil
	}
	const space = sx.String(" ")
	text := make(sx.Vector, 0, 2*len(tags)+2)
	for _, tag := range tags {
		text = append(text, space, wui.transformKeyValueText(key, tag, tag))
	}
	if len(tags) > 1 {
		text = append(text, space, wui.transformKeyValuesText(key, tags, "(all)"))
	}







|
|
|
|
















|

|







|








|

|




|





|




|










|

|
|











|











|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

package webui

import (
	"context"
	"errors"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) writeHTMLMetaValue(
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
) sx.Object {
	switch kt := meta.Type(key); kt {
	case meta.TypeCredential:
		return sx.MakeString(value)
	case meta.TypeEmpty:
		return sx.MakeString(value)
	case meta.TypeID:
		return wui.transformIdentifier(value, getTextTitle)
	case meta.TypeIDSet:
		return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
	case meta.TypeNumber:
		return wui.transformKeyValueText(key, value, value)
	case meta.TypeString:
		return sx.MakeString(value)
	case meta.TypeTagSet:
		return wui.transformTagSet(key, meta.ListFromValue(value))
	case meta.TypeTimestamp:
		if ts, ok := meta.TimeValue(value); ok {
			return sx.MakeList(
				sx.MakeSymbol("time"),
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(sx.MakeSymbol("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))),
				),
				sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(ts.Format("2006-01-02&nbsp;15:04:05"))),
			)
		}
		return sx.Nil()
	case meta.TypeURL:
		return wui.url2html(sx.MakeString(value))
	case meta.TypeWord:
		return wui.transformKeyValueText(key, value, value)
	case meta.TypeZettelmarkup:
		return wui.transformZmkMetadata(value, evalMetadata, gen)
	default:
		return sx.MakeList(shtml.SymSTRONG, sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name))
	}
}

func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
	text := sx.MakeString(val)
	zid, err := id.Parse(val)
	if err != nil {
		return text
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(zid.ZettelID())
		attrs := sx.Nil()
		if title != "" {
			attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.MakeString(title)))
		}
		attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String()))).Cons(sxhtml.SymAttr)
		return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA)
	case found == 0:
		return sx.MakeList(sx.MakeSymbol("s"), text)
	default: // case found < 0:
		return text
	}
}

func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
	if len(vals) == 0 {
		return nil
	}
	var space = sx.MakeString(" ")
	text := make(sx.Vector, 0, 2*len(vals))
	for _, val := range vals {
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
	}
	return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
}

func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
	if len(tags) == 0 {
		return nil
	}
	var space = sx.MakeString(" ")
	text := make(sx.Vector, 0, 2*len(tags)+2)
	for _, tag := range tags {
		text = append(text, space, wui.transformKeyValueText(key, tag, tag))
	}
	if len(tags) > 1 {
		text = append(text, space, wui.transformKeyValuesText(key, tags, "(all)"))
	}
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
}

func buildHref(ub *api.URLBuilder, text string) *sx.Pair {
	return sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.String(ub.String())),
		),
		sx.String(text),
	)
}

type evalMetadataFunc = func(string) ast.InlineSlice

func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
	return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }







|

|







137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
}

func buildHref(ub *api.URLBuilder, text string) *sx.Pair {
	return sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String())),
		),
		sx.MakeString(text),
	)
}

type evalMetadataFunc = func(string) ast.InlineSlice

func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
	return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }

Changes to web/adapter/webui/lists.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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package webui

import (
	"context"
	"io"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc {




	return func(w http.ResponseWriter, r *http.Request) {
		urlQuery := r.URL.Query()
		if wui.handleTagZettel(w, r, tagZettel, urlQuery) ||
			wui.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}
		q := adapter.GetQuery(urlQuery)
		q = q.SetDeterministic()
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(ctx, q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if len(actions) > 0 {
			if len(metaSeq) > 0 {
				for _, act := range actions {
					if act == api.RedirectAction {
						ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid.ZettelID())
						wui.redirectFound(w, r, ub)
						return
					}
				}
			}
			switch actions[0] {
			case api.AtomAction:
				wui.renderAtom(w, q, metaSeq)
				return
			case api.RSSAction:
				wui.renderRSS(ctx, w, q, metaSeq)
				return
			}

		}

		var content, endnotes *sx.Pair
		numEntries := 0
		if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil {
			enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, nil, api.KeyLang))
			content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}


		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "list",
			wui.rtConfig.Get(ctx, nil, api.KeyLang),
			wui.rtConfig.GetSiteName(), user)
		if q == nil {
			rb.bindString("heading", sx.String(wui.rtConfig.GetSiteName()))
		} else {
			var sb strings.Builder
			q.PrintHuman(&sb)
			rb.bindString("heading", sx.String(sb.String()))
		}
		rb.bindString("query-value", sx.String(q.String()))
		if tzl := q.GetMetaValues(api.KeyTags, false); len(tzl) > 0 {
			sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl)
			if !sx.IsNil(sxTzl) {
				rb.bindString("tag-zettel", sxTzl)
			}
			if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) {
				rb.bindString("create-tag-zettel", sxNoTzl)







<






|
|
|
|

<
<
<

<








|
>
>
>
>
|


















|
<
|
|
|
|
|
|
|
|
<
<
<
<
<
<
<
|
>
|
<


|
|








>

|
<
<
<

|



|

|







11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28



29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70







71
72
73

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package webui

import (
	"context"

	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"



	"zettelstore.de/z/evaluator"

	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,
	reIndex *usecase.ReIndex) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		urlQuery := r.URL.Query()
		if wui.handleTagZettel(w, r, tagZettel, urlQuery) ||
			wui.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}
		q := adapter.GetQuery(urlQuery)
		q = q.SetDeterministic()
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(ctx, q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if len(actions) > 0 && len(metaSeq) > 0 {

			for _, act := range actions {
				if act == api.RedirectAction {
					ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid.ZettelID())
					wui.redirectFound(w, r, ub)
					return
				}
			}
		}








		userLang := wui.getUserLang(ctx)


		var content, endnotes *sx.Pair
		numEntries := 0
		if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq); bn != nil {
			enc := wui.getSimpleHTMLEncoder(userLang)
			content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}

		siteName := wui.rtConfig.GetSiteName()
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "list", userLang, siteName, user)



		if q == nil {
			rb.bindString("heading", sx.MakeString(siteName))
		} else {
			var sb strings.Builder
			q.PrintHuman(&sb)
			rb.bindString("heading", sx.MakeString(sb.String()))
		}
		rb.bindString("query-value", sx.MakeString(q.String()))
		if tzl := q.GetMetaValues(api.KeyTags, false); len(tzl) > 0 {
			sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl)
			if !sx.IsNil(sxTzl) {
				rb.bindString("tag-zettel", sxTzl)
			}
			if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) {
				rb.bindString("create-tag-zettel", sxNoTzl)
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
		seed, found := q.GetSeed()
		if found {
			apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed))
		} else {
			seed = 0
		}
		if len(metaSeq) > 0 {
			rb.bindString("plain-url", sx.String(apiURL.String()))
			rb.bindString("data-url", sx.String(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String()))
			if wui.canCreate(ctx, user) {
				rb.bindString("create-url", sx.String(wui.createNewURL))
				rb.bindString("seed", sx.Int64(seed))
			}
		}
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) {
	slices.Reverse(tags)
	for _, tag := range tags {
		tag = meta.NormalizeTag(tag)
		if _, err := tagZettel.Run(ctx, tag); err == nil {







|
|

|











|







120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
		seed, found := q.GetSeed()
		if found {
			apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed))
		} else {
			seed = 0
		}
		if len(metaSeq) > 0 {
			rb.bindString("plain-url", sx.MakeString(apiURL.String()))
			rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String()))
			if wui.canCreate(ctx, user) {
				rb.bindString("create-url", sx.MakeString(wui.createNewURL))
				rb.bindString("seed", sx.Int64(seed))
			}
		}
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	})
}

func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) {
	slices.Reverse(tags)
	for _, tag := range tags {
		tag = meta.NormalizeTag(tag)
		if _, err := tagZettel.Run(ctx, tag); err == nil {
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
}

func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair {
	link := sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.String(u.String())),
		),
		sx.String(name),
	)
	if sxZtl != nil {
		sxZtl = sxZtl.Cons(sx.String(", "))
	}
	return sxZtl.Cons(link)
}

func (wui *WebUI) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var rssConfig rss.Configuration
	rssConfig.Setup(ctx, wui.rtConfig)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction {
		rssConfig.Title = strings.Join(actions[2:], " ")
	}
	data := rssConfig.Marshal(q, ml)

	adapter.PrepareHeader(w, rss.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
	}
	if err != nil {
		wui.log.Error().Err(err).Msg("unable to write RSS data")
	}
}

func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var atomConfig atom.Configuration
	atomConfig.Setup(wui.rtConfig)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction {
		atomConfig.Title = strings.Join(actions[2:], " ")
	}
	data := atomConfig.Marshal(q, ml)

	adapter.PrepareHeader(w, atom.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
	}
	if err != nil {
		wui.log.Error().Err(err).Msg("unable to write Atom data")
	}
}

func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)
	if tag == "" {
		return false
	}
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, tag)







|

|


|




<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188






































189
190
191
192
193
194
195
}

func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair {
	link := sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())),
		),
		sx.MakeString(name),
	)
	if sxZtl != nil {
		sxZtl = sxZtl.Cons(sx.MakeString(", "))
	}
	return sxZtl.Cons(link)
}







































func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)
	if tag == "" {
		return false
	}
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, tag)

Changes to web/adapter/webui/login.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

package webui

import (
	"context"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view,
// or to execute a logout.
func (wui *WebUI) MakeGetLoginOutHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
		if query.Has("logout") {
			wui.clearToken(r.Context(), w)
			wui.redirectFound(w, r, wui.NewURLBuilder('/'))
			return
		}
		wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
	}
}

func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
	env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil)
	rb.bindString("retry", sx.MakeBoolean(retry))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}
}

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user.
func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !wui.authz.WithAuth() {
			wui.redirectFound(w, r, wui.NewURLBuilder('/'))
			return
		}
		ctx := r.Context()
		ident, cred, ok := adapter.GetCredentialsViaForm(r)
		if !ok {







<
|








|
|







|



|










|
|







13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

package webui

import (
	"context"
	"net/http"


	"t73f.de/r/sx"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view,
// or to execute a logout.
func (wui *WebUI) MakeGetLoginOutHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
		if query.Has("logout") {
			wui.clearToken(r.Context(), w)
			wui.redirectFound(w, r, wui.NewURLBuilder('/'))
			return
		}
		wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
	})
}

func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
	env, rb := wui.createRenderEnv(ctx, "login", wui.getUserLang(ctx), "Login", nil)
	rb.bindString("retry", sx.MakeBoolean(retry))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}
}

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user.
func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !wui.authz.WithAuth() {
			wui.redirectFound(w, r, wui.NewURLBuilder('/'))
			return
		}
		ctx := r.Context()
		ident, cred, ok := adapter.GetCredentialsViaForm(r)
		if !ok {
71
72
73
74
75
76
77
78
79
		if token == nil {
			wui.renderLoginForm(wui.clearToken(ctx, w), w, true)
			return
		}

		wui.setToken(w, token)
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}







|

70
71
72
73
74
75
76
77
78
		if token == nil {
			wui.renderLoginForm(wui.clearToken(ctx, w), w, true)
			return
		}

		wui.setToken(w, token)
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	})
}

Deleted web/adapter/webui/rename_zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package webui

import (
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
)

// MakeGetRenameZettelHandler creates a new HTTP handler to display the
// HTML rename view of a zettel.
func (wui *WebUI) MakeGetRenameZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		z, err := getZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := z.Meta

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "rename",
			wui.rtConfig.Get(ctx, nil, api.KeyLang), "Rename Zettel "+m.Zid.String(), user)
		rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.RenameTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

// MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel.
func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		curZid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		if err = r.ParseForm(); err != nil {
			wui.log.Trace().Err(err).Msg("unable to read rename zettel form")
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form"))
			return
		}
		formCurZidStr := r.PostFormValue("curzid")
		if formCurZid, err1 := id.Parse(formCurZidStr); err1 != nil || formCurZid != curZid {
			if err1 != nil {
				wui.log.Trace().Str("formCurzid", formCurZidStr).Err(err1).Msg("unable to parse as zid")
			} else if formCurZid != curZid {
				wui.log.Trace().Zid(formCurZid).Zid(curZid).Msg("zid differ (form/url)")
			}
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form"))
			return
		}
		formNewZid := strings.TrimSpace(r.PostFormValue("newzid"))
		newZid, err := id.Parse(formNewZid)
		if err != nil {
			wui.reportError(
				ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid)))
			return
		}

		if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































Changes to web/adapter/webui/response.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package webui

import (
	"net/http"

	"zettelstore.de/client.fossil/api"
)

func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
	us := ub.String()
	wui.log.Debug().Str("uri", us).Msg("redirect")
	http.Redirect(w, r, us, http.StatusFound)
}







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package webui

import (
	"net/http"

	"t73f.de/r/zsc/api"
)

func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
	us := ub.String()
	wui.log.Debug().Str("uri", us).Msg("redirect")
	http.Redirect(w, r, us, http.StatusFound)
}

Changes to web/adapter/webui/sxn_code.go.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package webui

import (
	"context"
	"fmt"
	"io"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (id.Digraph, *sxeval.Binding, error) {
	// getMeta MUST currently use GetZettel, because GetMeta just uses the
	// Index, which might not be current.







|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package webui

import (
	"context"
	"fmt"
	"io"

	"t73f.de/r/sx/sxeval"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (id.Digraph, *sxeval.Binding, error) {
	// getMeta MUST currently use GetZettel, because GetMeta just uses the
	// Index, which might not be current.
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
type getMetaFunc func(context.Context, id.Zid) (*meta.Meta, error)

func buildSxnCodeDigraph(ctx context.Context, startZid id.Zid, getMeta getMetaFunc) id.Digraph {
	m, err := getMeta(ctx, startZid)
	if err != nil {
		return nil
	}
	var marked id.Set
	stack := []*meta.Meta{m}
	dg := id.Digraph(nil).AddVertex(startZid)
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr.Zid) {
			continue







|







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
type getMetaFunc func(context.Context, id.Zid) (*meta.Meta, error)

func buildSxnCodeDigraph(ctx context.Context, startZid id.Zid, getMeta getMetaFunc) id.Digraph {
	m, err := getMeta(ctx, startZid)
	if err != nil {
		return nil
	}
	var marked *id.Set
	stack := []*meta.Meta{m}
	dg := id.Digraph(nil).AddVertex(startZid)
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr.Zid) {
			continue

Changes to web/adapter/webui/template.go.

15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

108
109
110
111
112
113
114

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/url"


	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxbuiltins"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) createRenderBinding() *sxeval.Binding {
	root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3)
	for _, syntax := range specials {
		root.BindSpecial(syntax)
	}
	for _, b := range builtins {
		root.BindBuiltin(b)
	}


	root.BindBuiltin(&sxeval.Builtin{
		Name:     "url-to-html",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn: func(_ *sxeval.Environment, args sx.Vector) (sx.Object, error) {
			text, err := sxbuiltins.GetString(args, 0)
			if err != nil {
				return nil, err
			}
			return wui.url2html(text), nil
		},
	})
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "zid-content-path",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn: func(_ *sxeval.Environment, args sx.Vector) (sx.Object, error) {
			s, err := sxbuiltins.GetString(args, 0)
			if err != nil {
				return nil, err
			}
			zid, err := id.Parse(string(s))
			if err != nil {
				return nil, fmt.Errorf("parsing zettel identifier %q: %w", s, err)
			}
			ub := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
			return sx.String(ub.String()), nil
		},
	})
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "query->url",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn: func(_ *sxeval.Environment, args sx.Vector) (sx.Object, error) {
			qs, err := sxbuiltins.GetString(args, 0)
			if err != nil {
				return nil, err
			}
			u := wui.NewURLBuilder('h').AppendQuery(string(qs))
			return sx.String(u.String()), nil
		},
	})
	root.Freeze()
	return root
}

var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefVarS, &sxbuiltins.DefConstS, // defvar, defconst
		&sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda
		&sxbuiltins.SetXS,     // set!
		&sxbuiltins.IfS,       // if
		&sxbuiltins.BeginS,    // begin
		&sxbuiltins.DefMacroS, // defmacro

	}
	builtins = []*sxeval.Builtin{
		&sxbuiltins.Equal,                // =
		&sxbuiltins.NumGreater,           // >
		&sxbuiltins.NullP,                // null?
		&sxbuiltins.PairP,                // pair?
		&sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr







>

|
|
|
|
|
|
|



















>
>





|
|











|
|



|

|


|







|
|



|
|










|





>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/url"
	"slices"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) createRenderBinding() *sxeval.Binding {
	root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3)
	for _, syntax := range specials {
		root.BindSpecial(syntax)
	}
	for _, b := range builtins {
		root.BindBuiltin(b)
	}
	_ = root.Bind(sx.MakeSymbol("NIL"), sx.Nil())
	_ = root.Bind(sx.MakeSymbol("T"), sx.MakeSymbol("T"))
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "url-to-html",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			text, err := sxbuiltins.GetString(arg, 0)
			if err != nil {
				return nil, err
			}
			return wui.url2html(text), nil
		},
	})
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "zid-content-path",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			s, err := sxbuiltins.GetString(arg, 0)
			if err != nil {
				return nil, err
			}
			zid, err := id.Parse(s.GetValue())
			if err != nil {
				return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err)
			}
			ub := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
			return sx.MakeString(ub.String()), nil
		},
	})
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "query->url",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			qs, err := sxbuiltins.GetString(arg, 0)
			if err != nil {
				return nil, err
			}
			u := wui.NewURLBuilder('h').AppendQuery(qs.GetValue())
			return sx.MakeString(u.String()), nil
		},
	})
	root.Freeze()
	return root
}

var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefVarS,                     // defvar
		&sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda
		&sxbuiltins.SetXS,     // set!
		&sxbuiltins.IfS,       // if
		&sxbuiltins.BeginS,    // begin
		&sxbuiltins.DefMacroS, // defmacro
		&sxbuiltins.LetS,      // let
	}
	builtins = []*sxeval.Builtin{
		&sxbuiltins.Equal,                // =
		&sxbuiltins.NumGreater,           // >
		&sxbuiltins.NullP,                // null?
		&sxbuiltins.PairP,                // pair?
		&sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
		&sxbuiltins.Defined,        // defined?
		&sxbuiltins.CurrentBinding, // current-binding
		&sxbuiltins.BindingLookup,  // binding-lookup
	}
)

func (wui *WebUI) url2html(text sx.String) sx.Object {
	if u, errURL := url.Parse(string(text)); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				shtml.SymA,
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(shtml.SymAttrHref, sx.String(us)),
					sx.Cons(shtml.SymAttrTarget, sx.String("_blank")),
					sx.Cons(shtml.SymAttrRel, sx.String("noopener noreferrer")),
				),
				text)
		}
	}
	return text
}








|





|
|
|







129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
		&sxbuiltins.Defined,        // defined?
		&sxbuiltins.CurrentBinding, // current-binding
		&sxbuiltins.BindingLookup,  // binding-lookup
	}
)

func (wui *WebUI) url2html(text sx.String) sx.Object {
	if u, errURL := url.Parse(text.GetValue()); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				shtml.SymA,
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(shtml.SymAttrHref, sx.MakeString(us)),
					sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")),
					sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")),
				),
				text)
		}
	}
	return text
}

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197

// createRenderEnv creates a new environment and populates it with all relevant data for the base template.
func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Binding, renderBinder) {
	userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)
	parentEnv, err := wui.getParentEnv(ctx)
	bind := parentEnv.MakeChildBinding(name, 128)
	rb := makeRenderBinder(bind, err)
	rb.bindString("lang", sx.String(lang))
	rb.bindString("css-base-url", sx.String(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.String(wui.cssUserURL))
	rb.bindString("title", sx.String(title))
	rb.bindString("home-url", sx.String(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
	rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid))
	rb.bindString("user-zettel-url", sx.String(userZettelURL))
	rb.bindString("user-ident", sx.String(userIdent))
	rb.bindString("login-url", sx.String(wui.loginURL))
	rb.bindString("logout-url", sx.String(wui.logoutURL))
	rb.bindString("list-zettel-url", sx.String(wui.listZettelURL))
	rb.bindString("list-roles-url", sx.String(wui.listRolesURL))
	rb.bindString("list-tags-url", sx.String(wui.listTagsURL))
	if wui.canRefresh(user) {
		rb.bindString("refresh-url", sx.String(wui.refreshURL))
	}
	rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user))
	rb.bindString("search-url", sx.String(wui.searchURL))
	rb.bindString("query-key-query", sx.String(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.String(api.QueryKeySeed))
	rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
	rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
	rb.bindSymbol(symMetaHeader, sx.Nil())
	rb.bindSymbol(symDetail, sx.Nil())
	return bind, rb
}








|
|
|
|
|


|
|
|
|
|
|
|

|


|
|
|







167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

// createRenderEnv creates a new environment and populates it with all relevant data for the base template.
func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Binding, renderBinder) {
	userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)
	parentEnv, err := wui.getParentEnv(ctx)
	bind := parentEnv.MakeChildBinding(name, 128)
	rb := makeRenderBinder(bind, err)
	rb.bindString("lang", sx.MakeString(lang))
	rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL))
	rb.bindString("title", sx.MakeString(title))
	rb.bindString("home-url", sx.MakeString(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
	rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid))
	rb.bindString("user-zettel-url", sx.MakeString(userZettelURL))
	rb.bindString("user-ident", sx.MakeString(userIdent))
	rb.bindString("login-url", sx.MakeString(wui.loginURL))
	rb.bindString("logout-url", sx.MakeString(wui.logoutURL))
	rb.bindString("list-zettel-url", sx.MakeString(wui.listZettelURL))
	rb.bindString("list-roles-url", sx.MakeString(wui.listRolesURL))
	rb.bindString("list-tags-url", sx.MakeString(wui.listTagsURL))
	if wui.canRefresh(user) {
		rb.bindString("refresh-url", sx.MakeString(wui.refreshURL))
	}
	rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user))
	rb.bindString("search-url", sx.MakeString(wui.searchURL))
	rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed))
	rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
	rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
	rb.bindSymbol(symMetaHeader, sx.Nil())
	rb.bindSymbol(symDetail, sx.Nil())
	return bind, rb
}

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
}
func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) {
	if rb.err == nil {
		rb.err = rb.binding.Bind(sym, obj)
	}
}
func (rb *renderBinder) bindKeyValue(key string, value string) {
	rb.bindString("meta-"+key, sx.String(value))
	if kt := meta.Type(key); kt.IsSet {
		rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value)))
	}
}
func (rb *renderBinder) rebindResolved(key, defKey string) {
	if rb.err == nil {
		if obj, found := rb.binding.Resolve(sx.MakeSymbol(key)); found {
			rb.bindString(defKey, obj)
		}
	}
}

func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
	strZid := m.Zid.String()
	apiZid := api.ZettelID(strZid)
	newURLBuilder := wui.NewURLBuilder

	rb.bindString("zid", sx.String(strZid))
	rb.bindString("web-url", sx.String(newURLBuilder('h').SetZid(apiZid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.String(newURLBuilder('e').SetZid(apiZid).String()))
	}
	rb.bindString("info-url", sx.String(newURLBuilder('i').SetZid(apiZid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}
		rb.bindString("version-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		rb.bindString("child-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String()))
		rb.bindString("folge-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canRename(ctx, user, m) {
		rb.bindString("rename-url", sx.String(newURLBuilder('b').SetZid(apiZid).String()))
	}
	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.String(newURLBuilder('d').SetZid(apiZid).String()))
	}
	if val, found := m.Get(api.KeyUselessFiles); found {
		rb.bindString("useless", sx.Cons(sx.String(val), nil))
	}
	queryContext := strZid + " " + api.ContextDirective
	rb.bindString("context-url", sx.String(newURLBuilder('h').AppendQuery(queryContext).String()))
	queryContext += " " + api.FullDirective
	rb.bindString("context-full-url", sx.String(newURLBuilder('h').AppendQuery(queryContext).String()))
	if wui.canRefresh(user) {
		rb.bindString("reindex-url", sx.String(newURLBuilder('h').AppendQuery(
			strZid+" "+api.IdentDirective+api.ActionSeparator+api.ReIndexAction).String()))
	}

	// Ensure to have title, role, tags, and syntax included as "meta-*"
	rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, ""))
	rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, ""))
	rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, ""))
	rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
	var metaPairs sx.ListBuilder
	for _, p := range m.ComputedPairs() {
		key, value := p.Key, p.Value
		metaPairs.Add(sx.Cons(sx.String(key), sx.String(value)))
		rb.bindKeyValue(key, value)
	}
	rb.bindString("metapairs", metaPairs.List())
}

func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) {
	if !wui.canCreate(ctx, user) {
		return nil
	}
	ctx = box.NoEnrichContext(ctx)
	menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}
	refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig))

	for i := len(refs) - 1; i >= 0; i-- {



		zid, err2 := id.Parse(refs[i].URL.Path)
		if err2 != nil {
			continue
		}
		z, err2 := wui.box.GetZettel(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, z.Meta) {
			continue
		}
		text := sx.String(parser.NormalizedSpacedText(z.Meta.GetTitle()))
		link := sx.String(wui.NewURLBuilder('c').SetZid(zid.ZettelID()).
			AppendKVQuery(queryKeyAction, valueActionNew).String())

		lst = lst.Cons(sx.Cons(text, link))
	}
	return lst
}

func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
	if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)).SetUnique("footer-")
			if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil {
				if content != nil && endnotes != nil {
					content.LastPair().SetCdr(sx.Cons(endnotes, nil))
				}
				return content
			}
		}







|

















|
|

|

|


|

|
|
|

<
<
<

|


|


|

|

|











|














|
>
|
>
>
>
|










|
|






>

|

|







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
}
func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) {
	if rb.err == nil {
		rb.err = rb.binding.Bind(sym, obj)
	}
}
func (rb *renderBinder) bindKeyValue(key string, value string) {
	rb.bindString("meta-"+key, sx.MakeString(value))
	if kt := meta.Type(key); kt.IsSet {
		rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value)))
	}
}
func (rb *renderBinder) rebindResolved(key, defKey string) {
	if rb.err == nil {
		if obj, found := rb.binding.Resolve(sx.MakeSymbol(key)); found {
			rb.bindString(defKey, obj)
		}
	}
}

func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
	strZid := m.Zid.String()
	apiZid := api.ZettelID(strZid)
	newURLBuilder := wui.NewURLBuilder

	rb.bindString("zid", sx.MakeString(strZid))
	rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(apiZid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String()))
	}
	rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}
		rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		rb.bindString("sequel-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionSequel).String()))
		rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}



	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(apiZid).String()))
	}
	if val, found := m.Get(api.KeyUselessFiles); found {
		rb.bindString("useless", sx.Cons(sx.MakeString(val), nil))
	}
	queryContext := strZid + " " + api.ContextDirective
	rb.bindString("context-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String()))
	queryContext += " " + api.FullDirective
	rb.bindString("context-full-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String()))
	if wui.canRefresh(user) {
		rb.bindString("reindex-url", sx.MakeString(newURLBuilder('h').AppendQuery(
			strZid+" "+api.IdentDirective+api.ActionSeparator+api.ReIndexAction).String()))
	}

	// Ensure to have title, role, tags, and syntax included as "meta-*"
	rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, ""))
	rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, ""))
	rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, ""))
	rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
	var metaPairs sx.ListBuilder
	for _, p := range m.ComputedPairs() {
		key, value := p.Key, p.Value
		metaPairs.Add(sx.Cons(sx.MakeString(key), sx.MakeString(value)))
		rb.bindKeyValue(key, value)
	}
	rb.bindString("metapairs", metaPairs.List())
}

func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) {
	if !wui.canCreate(ctx, user) {
		return nil
	}
	ctx = box.NoEnrichContext(ctx)
	menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}
	links := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig))
	for _, ln := range slices.Backward(links) {
		ref := ln.Ref
		if !ref.IsZettel() {
			continue
		}
		zid, err2 := id.Parse(ref.URL.Path)
		if err2 != nil {
			continue
		}
		z, err2 := wui.box.GetZettel(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, z.Meta) {
			continue
		}
		text := sx.MakeString(parser.NormalizedSpacedText(z.Meta.GetTitle()))
		link := sx.MakeString(wui.NewURLBuilder('c').SetZid(zid.ZettelID()).
			AppendKVQuery(queryKeyAction, valueActionNew).String())

		lst = lst.Cons(sx.Cons(text, link))
	}
	return lst
}

func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
	if footerZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, api.KeyLang)).SetUnique("footer-")
			if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil {
				if content != nil && endnotes != nil {
					content.LastPair().SetCdr(sx.Cons(endnotes, nil))
				}
				return content
			}
		}
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
		return err
	}
	if msg := wui.log.Debug(); msg != nil {
		// pageObj.String() can be expensive to calculate.
		msg.Str("page", pageObj.String()).Msg("render")
	}

	gen := sxhtml.NewGenerator(sxhtml.WithNewline)
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	if _, err = w.Write(sb.Bytes()); err != nil {
		wui.log.Error().Err(err).Msg("Unable to write HTML via template")
	}
	return nil // No error reporting, since we do not know what happended during write to client.
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {

	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	} else {
		wui.log.Debug().Err(err).Msg("reportError")
	}
	user := server.GetUser(ctx)
	env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user)
	rb.bindString("heading", sx.String(http.StatusText(code)))
	rb.bindString("message", sx.String(text))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ErrorTemplateZid, env)
	}
	errSx := rb.err
	if errSx == nil {
		return
	}







|













>








|
|







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
		return err
	}
	if msg := wui.log.Debug(); msg != nil {
		// pageObj.String() can be expensive to calculate.
		msg.Str("page", pageObj.String()).Msg("render")
	}

	gen := sxhtml.NewGenerator().SetNewline()
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	if _, err = w.Write(sb.Bytes()); err != nil {
		wui.log.Error().Err(err).Msg("Unable to write HTML via template")
	}
	return nil // No error reporting, since we do not know what happended during write to client.
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	ctx = context.WithoutCancel(ctx) // Ignore any cancel / timeouts to write an error message.
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	} else {
		wui.log.Debug().Err(err).Msg("reportError")
	}
	user := server.GetUser(ctx)
	env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user)
	rb.bindString("heading", sx.MakeString(http.StatusText(code)))
	rb.bindString("message", sx.MakeString(text))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ErrorTemplateZid, env)
	}
	errSx := rb.err
	if errSx == nil {
		return
	}
444
445
446
447
448
449
450
451
452
453
454

func makeStringList(sl []string) *sx.Pair {
	if len(sl) == 0 {
		return nil
	}
	result := sx.Nil()
	for i := len(sl) - 1; i >= 0; i-- {
		result = result.Cons(sx.String(sl[i]))
	}
	return result
}







|



451
452
453
454
455
456
457
458
459
460
461

func makeStringList(sl []string) *sx.Pair {
	if len(sl) == 0 {
		return nil
	}
	result := sx.Nil()
	for i := len(sl) - 1; i >= 0; i-- {
		result = result.Cons(sx.MakeString(sl[i]))
	}
	return result
}

Changes to web/adapter/webui/webui.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

import (
	"context"
	"net/http"
	"sync"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"







|
|
|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

import (
	"context"
	"net/http"
	"sync"
	"time"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//
// Note: these function must not do auth checking.
type webuiBox interface {
	CanCreateZettel(context.Context) bool
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	CanUpdateZettel(context.Context, zettel.Zettel) bool
	AllowRenameZettel(context.Context, id.Zid) bool
	CanDeleteZettel(context.Context, id.Zid) bool
}

// New creates a new WebUI struct.
func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI {
	loginoutBase := ab.NewURLBuilder('i')







<







78
79
80
81
82
83
84

85
86
87
88
89
90
91
//
// Note: these function must not do auth checking.
type webuiBox interface {
	CanCreateZettel(context.Context) bool
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	CanUpdateZettel(context.Context, zettel.Zettel) bool

	CanDeleteZettel(context.Context, id.Zid) bool
}

// New creates a new WebUI struct.
func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI {
	loginoutBase := ab.NewURLBuilder('i')
116
117
118
119
120
121
122
123
124
125
126
127
128
129







130
131
132
133
134
135
136
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),

		zettelBinding: nil,
		genHTML:       sxhtml.NewGenerator(sxhtml.WithNewline),
	}
	wui.rootBinding = wui.createRenderBinding()
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}








var (
	symDetail     = sx.MakeSymbol("DETAIL")
	symMetaHeader = sx.MakeSymbol("META-HEADER")
)

func (wui *WebUI) observe(ci box.UpdateInfo) {







|






>
>
>
>
>
>
>







115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),

		zettelBinding: nil,
		genHTML:       sxhtml.NewGenerator().SetNewline(),
	}
	wui.rootBinding = wui.createRenderBinding()
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) getConfig(ctx context.Context, m *meta.Meta, key string) string {
	return wui.rtConfig.Get(ctx, m, key)
}
func (wui *WebUI) getUserLang(ctx context.Context) string {
	return wui.getConfig(ctx, nil, api.KeyLang)
}

var (
	symDetail     = sx.MakeSymbol("DETAIL")
	symMetaHeader = sx.MakeSymbol("META-HEADER")
)

func (wui *WebUI) observe(ci box.UpdateInfo) {
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

func (wui *WebUI) canWrite(
	ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool {
	return wui.policy.CanWrite(user, meta, meta) &&
		wui.box.CanUpdateZettel(ctx, zettel.Zettel{Meta: meta, Content: content})
}

func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool {
	return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid)
}

func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool {
	return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid)
}

func (wui *WebUI) canRefresh(user *meta.Meta) bool {
	return wui.policy.CanRefresh(user)
}







<
<
<
<







178
179
180
181
182
183
184




185
186
187
188
189
190
191

func (wui *WebUI) canWrite(
	ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool {
	return wui.policy.CanWrite(user, meta, meta) &&
		wui.box.CanUpdateZettel(ctx, zettel.Zettel{Meta: meta, Content: content})
}





func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool {
	return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid)
}

func (wui *WebUI) canRefresh(user *meta.Meta) bool {
	return wui.policy.CanRefresh(user)
}

Changes to web/content/content.go.

15
16
17
18
19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
// It translates syntax values into content types, and vice versa.
package content

import (
	"mime"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)


const (
	UnknownMIME  = "application/octet-stream"
	mimeGIF      = "image/gif"
	mimeHTML     = "text/html; charset=utf-8"
	mimeJPEG     = "image/jpeg"
	mimeMarkdown = "text/markdown; charset=utf-8"
	PlainText    = "text/plain; charset=utf-8"







|




>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// It translates syntax values into content types, and vice versa.
package content

import (
	"mime"
	"net/http"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)

// Some MIME encoding values.
const (
	UnknownMIME  = "application/octet-stream"
	mimeGIF      = "image/gif"
	mimeHTML     = "text/html; charset=utf-8"
	mimeJPEG     = "image/jpeg"
	mimeMarkdown = "text/markdown; charset=utf-8"
	PlainText    = "text/plain; charset=utf-8"
96
97
98
99
100
101
102


103
104
105
106
107
108
109
	"text/plain":    meta.SyntaxText,

	// Additional syntaxes
	"application/pdf": "pdf",
	"text/javascript": "js",
}



func SyntaxFromMIME(m string, data []byte) string {
	mt, _, _ := mime.ParseMediaType(m)
	if syntax, found := mime2syntax[mt]; found {
		return syntax
	}
	if len(data) > 0 {
		ct := http.DetectContentType(data)







>
>







97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
	"text/plain":    meta.SyntaxText,

	// Additional syntaxes
	"application/pdf": "pdf",
	"text/javascript": "js",
}

// SyntaxFromMIME returns the syntax for a zettel based on MIME encoding value
// and the actual data.
func SyntaxFromMIME(m string, data []byte) string {
	mt, _, _ := mime.ParseMediaType(m)
	if syntax, found := mime2syntax[mt]; found {
		return syntax
	}
	if len(data) > 0 {
		ct := http.DetectContentType(data)

Changes to web/server/impl/http.go.

20
21
22
23
24
25
26
27
28
29
30
31
32
33


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58

59
60
61
62
63
64
65
	"time"
)

// Server timeout values
const (
	shutdownTimeout = 5 * time.Second
	readTimeout     = 5 * time.Second
	writeTimeout    = 10 * time.Second
	idleTimeout     = 120 * time.Second
)

// httpServer is a HTTP server.
type httpServer struct {
	http.Server


}

// initializeHTTPServer creates a new HTTP server object.
func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) {
	if addr == "" {
		addr = ":http"
	}
	srv.Server = http.Server{
		Addr:    addr,
		Handler: handler,

		// See: https://blog.cloudflare.com/exposing-go-on-the-internet/
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
		IdleTimeout:  idleTimeout,
	}

}

// SetDebug enables debugging goroutines that are started by the server.
// Basically, just the timeout values are reset. This method should be called
// before running the server.
func (srv *httpServer) SetDebug() {
	srv.ReadTimeout = 0
	srv.WriteTimeout = 0
	srv.IdleTimeout = 0

}

// Run starts the web server, but does not wait for its completion.
func (srv *httpServer) Run() error {
	ln, err := net.Listen("tcp", srv.Addr)
	if err != nil {
		return err







|






>
>









|



|


>









>







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
	"time"
)

// Server timeout values
const (
	shutdownTimeout = 5 * time.Second
	readTimeout     = 5 * time.Second
	writeTimeout    = 20 * time.Second
	idleTimeout     = 120 * time.Second
)

// httpServer is a HTTP server.
type httpServer struct {
	http.Server

	origHandler http.Handler
}

// initializeHTTPServer creates a new HTTP server object.
func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) {
	if addr == "" {
		addr = ":http"
	}
	srv.Server = http.Server{
		Addr:    addr,
		Handler: http.TimeoutHandler(handler, writeTimeout, "Timeout"),

		// See: https://blog.cloudflare.com/exposing-go-on-the-internet/
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout + 500*time.Millisecond, // Give some time to detect timeout and to write an appropriate error message.
		IdleTimeout:  idleTimeout,
	}
	srv.origHandler = handler
}

// SetDebug enables debugging goroutines that are started by the server.
// Basically, just the timeout values are reset. This method should be called
// before running the server.
func (srv *httpServer) SetDebug() {
	srv.ReadTimeout = 0
	srv.WriteTimeout = 0
	srv.IdleTimeout = 0
	srv.Handler = srv.origHandler
}

// Run starts the web server, but does not wait for its completion.
func (srv *httpServer) Run() error {
	ln, err := net.Listen("tcp", srv.Addr)
	if err != nil {
		return err

Changes to web/server/impl/impl.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37













38
39
40
41
42
43
44
45








46
47
48
49
50
51
52
53
54
package impl

import (
	"context"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
)

type myServer struct {
	log              *logger.Logger
	baseURL          string
	server           httpServer
	router           httpRouter
	persistentCookie bool
	secureCookie     bool
}














// New creates a new web server.
func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server {
	srv := myServer{
		log:              log,
		baseURL:          baseURL,
		persistentCookie: persistentCookie,
		secureCookie:     secureCookie,
	}








	srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth)
	srv.server.initializeHTTPServer(listenAddr, &srv.router)
	return &srv
}

func (srv *myServer) Handle(pattern string, handler http.Handler) {
	srv.router.Handle(pattern, handler)
}
func (srv *myServer) AddListRoute(key byte, method server.Method, handler http.Handler) {







|















>
>
>
>
>
>
>
>
>
>
>
>
>

|

|
|
|
|

>
>
>
>
>
>
>
>
|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package impl

import (
	"context"
	"net/http"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
)

type myServer struct {
	log              *logger.Logger
	baseURL          string
	server           httpServer
	router           httpRouter
	persistentCookie bool
	secureCookie     bool
}

// ServerData contains the data needed to configure a server.
type ServerData struct {
	Log              *logger.Logger
	ListenAddr       string
	BaseURL          string
	URLPrefix        string
	MaxRequestSize   int64
	Auth             auth.TokenManager
	PersistentCookie bool
	SecureCookie     bool
	Profiling        bool
}

// New creates a new web server.
func New(sd ServerData) server.Server {
	srv := myServer{
		log:              sd.Log,
		baseURL:          sd.BaseURL,
		persistentCookie: sd.PersistentCookie,
		secureCookie:     sd.SecureCookie,
	}

	rd := routerData{
		log:            sd.Log,
		urlPrefix:      sd.URLPrefix,
		maxRequestSize: sd.MaxRequestSize,
		auth:           sd.Auth,
		profiling:      sd.Profiling,
	}
	srv.router.initializeRouter(rd)
	srv.server.initializeHTTPServer(sd.ListenAddr, &srv.router)
	return &srv
}

func (srv *myServer) Handle(pattern string, handler http.Handler) {
	srv.router.Handle(pattern, handler)
}
func (srv *myServer) AddListRoute(key byte, method server.Method, handler http.Handler) {

Changes to web/server/impl/router.go.

12
13
14
15
16
17
18

19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57








58
59
60
61
62
63
64
65
66
67
















68
69
70
71
72
73
74
//-----------------------------------------------------------------------------

package impl

import (
	"io"
	"net/http"

	"regexp"

	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
)

type (
	methodHandler [server.MethodLAST]http.Handler
	routingTable  [256]*methodHandler
)

var mapMethod = map[string]server.Method{
	http.MethodHead:   server.MethodHead,
	http.MethodGet:    server.MethodGet,
	http.MethodPost:   server.MethodPost,
	http.MethodPut:    server.MethodPut,
	http.MethodDelete: server.MethodDelete,
	api.MethodMove:    server.MethodMove,
}

// httpRouter handles all routing for zettelstore.
type httpRouter struct {
	log         *logger.Logger
	urlPrefix   string
	auth        auth.TokenManager
	minKey      byte
	maxKey      byte
	reURL       *regexp.Regexp
	listTable   routingTable
	zettelTable routingTable
	ur          server.UserRetriever
	mux         *http.ServeMux
	maxReqSize  int64
}









// initializeRouter creates a new, empty router with the given root handler.
func (rt *httpRouter) initializeRouter(log *logger.Logger, urlPrefix string, maxRequestSize int64, auth auth.TokenManager) {
	rt.log = log
	rt.urlPrefix = urlPrefix
	rt.auth = auth
	rt.minKey = 255
	rt.maxKey = 0
	rt.reURL = regexp.MustCompile("^$")
	rt.mux = http.NewServeMux()
	rt.maxReqSize = maxRequestSize
















}

func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) {
	// Set minKey and maxKey; re-calculate regexp.
	if key < rt.minKey || rt.maxKey < key {
		if key < rt.minKey {
			rt.minKey = key







>

>


<

















<

















>
>
>
>
>
>
>
>

|
|
|
|




|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







12
13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//-----------------------------------------------------------------------------

package impl

import (
	"io"
	"net/http"
	"net/http/pprof"
	"regexp"
	rtprf "runtime/pprof"
	"strings"


	"zettelstore.de/z/auth"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
)

type (
	methodHandler [server.MethodLAST]http.Handler
	routingTable  [256]*methodHandler
)

var mapMethod = map[string]server.Method{
	http.MethodHead:   server.MethodHead,
	http.MethodGet:    server.MethodGet,
	http.MethodPost:   server.MethodPost,
	http.MethodPut:    server.MethodPut,
	http.MethodDelete: server.MethodDelete,

}

// httpRouter handles all routing for zettelstore.
type httpRouter struct {
	log         *logger.Logger
	urlPrefix   string
	auth        auth.TokenManager
	minKey      byte
	maxKey      byte
	reURL       *regexp.Regexp
	listTable   routingTable
	zettelTable routingTable
	ur          server.UserRetriever
	mux         *http.ServeMux
	maxReqSize  int64
}

type routerData struct {
	log            *logger.Logger
	urlPrefix      string
	maxRequestSize int64
	auth           auth.TokenManager
	profiling      bool
}

// initializeRouter creates a new, empty router with the given root handler.
func (rt *httpRouter) initializeRouter(rd routerData) {
	rt.log = rd.log
	rt.urlPrefix = rd.urlPrefix
	rt.auth = rd.auth
	rt.minKey = 255
	rt.maxKey = 0
	rt.reURL = regexp.MustCompile("^$")
	rt.mux = http.NewServeMux()
	rt.maxReqSize = rd.maxRequestSize

	if rd.profiling {
		rt.setRuntimeProfiling()
	}
}

func (rt *httpRouter) setRuntimeProfiling() {
	rt.mux.HandleFunc("GET /rtp/", pprof.Index)
	for _, profile := range rtprf.Profiles() {
		name := profile.Name()
		rt.mux.Handle("GET /rtp/"+name, pprof.Handler(name))
	}
	rt.mux.HandleFunc("GET /rtp/cmdline", pprof.Cmdline)
	rt.mux.HandleFunc("GET /rtp/profile", pprof.Profile)
	rt.mux.HandleFunc("GET /rtp/symbol", pprof.Symbol)
	rt.mux.HandleFunc("GET /rtp/trace", pprof.Trace)
}

func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) {
	// Set minKey and maxKey; re-calculate regexp.
	if key < rt.minKey || rt.maxKey < key {
		if key < rt.minKey {
			rt.minKey = key

Changes to web/server/server.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package server

import (
	"context"
	"net/http"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// UserRetriever allows to retrieve user data based on a given zettel identifier.
type UserRetriever interface {
	GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error)
}

// Method enumerates the allowed HTTP methods.
type Method uint8

// Values for method type
const (
	MethodGet Method = iota
	MethodHead
	MethodPost
	MethodPut
	MethodMove
	MethodDelete
	MethodLAST // must always be the last one
)

// Router allows to state routes for various URL paths.
type Router interface {
	Handle(pattern string, handler http.Handler)







|


















<







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
package server

import (
	"context"
	"net/http"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// UserRetriever allows to retrieve user data based on a given zettel identifier.
type UserRetriever interface {
	GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error)
}

// Method enumerates the allowed HTTP methods.
type Method uint8

// Values for method type
const (
	MethodGet Method = iota
	MethodHead
	MethodPost
	MethodPut

	MethodDelete
	MethodLAST // must always be the last one
)

// Router allows to state routes for various URL paths.
type Router interface {
	Handle(pattern string, handler http.Handler)

Changes to www/build.md.

1

2

3
4
5
6
7
8
9

10
11
12
13
14
15
16
# How to build Zettelstore

## Prerequisites

You must install the following software:

* A current, supported [release of Go](https://go.dev/doc/devel/release),
* [staticcheck](https://staticcheck.io/),
* [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow),
* [unparam](https://mvdan.cc/unparam),
* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),

* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).

See folder `docs/development` (a zettel box) for details.

## Clone the repository
Most of this is covered by the excellent Fossil

>

>







>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# How to build Zettelstore

## Prerequisites

You must install the following software:

* A current, supported [release of Go](https://go.dev/doc/devel/release),
* [staticcheck](https://staticcheck.io/),
* [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow),
* [unparam](https://mvdan.cc/unparam),
* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),
* [revive](https://revive.run/),
* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).

See folder `docs/development` (a zettel box) for details.

## Clone the repository
Most of this is covered by the excellent Fossil
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80

81

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

## Tools to build, test, and manage
In the directory `tools` there are some Go files to automate most aspects of
building and testing, (hopefully) platform-independent.

The build script is called as:

```
go run tools/build/build.go [-v] COMMAND
```

The flag `-v` enables the verbose mode.
It outputs all commands called by the tool.

Some important `COMMAND`s are:

* `build`: builds the software with correct version information and puts it
  into a freshly created directory `bin`.
* `check`: checks the current state of the working directory to be ready for
  release (or commit).
* `version`: prints the current version information.

Therefore, the easiest way to build your own version of the Zettelstore
software is to execute the command

```
go run tools/build/build.go build
```

In case of errors, please send the output of the verbose execution:

```
go run tools/build/build.go -v build
```

Other tools are:

* `go run tools/clean/clean.go` cleans your Go development worspace.
* `go run tools/check/check.go` executes all linters and unit tests.
  If you add the option `-r` linters are more strict, to be used for a
  release version.
* `go run tools/devtools/devtools.go` install all needed software (see above).
* `go run tools/htmllint/htmllint.go [URL]` checks all generated HTML of a
  Zettelstore accessible at the given URL (default: http://localhost:23123).
* `go run tools/testapi/testapi.go` tests the API against a running
  Zettelstore, which is started automatically.

## A note on the use of Fossil

Zettelstore is managed by the Fossil version control system.
Fossil is an alternative to the ubiquitous Git version control system.
However, Go seems to prefer Git and popular platforms that just support Git.

Some dependencies of Zettelstore, namely [Zettelstore
client](https://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are
also managed by Fossil.

Depending on your development setup, some error messages might occur.


If the error message mentions an environment variable called `GOVCS` you should
set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous
to `GOVCS=*:all`).
Since the Go build system is coupled with Git and some special platforms, you
allow ot to download a Fossil repository from the host `zettelstore.de`.
The build tool set `GOVCS` to the right value, but you may use other `go`
commands that try to download a Fossil repository.

On some operating systems, namely Termux on Android, an error message might
state that an user cannot be determined (`cannot determine user`).
In this case, Fossil is allowed to download the repository, but cannot
associate it with an user name.
Set the environment variable `USER` to any user name, like:
`USER=nobody go run tools/build.go build`.







<
|
<















<
|
<



<
|
<



|










>
|
|
|


|
<
>
|
>



<
|
|
|
|


|
|
<
|

29
30
31
32
33
34
35

36

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

52

53
54
55

56

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

78
79
80
81
82
83

84
85
86
87
88
89
90
91

92
93

## Tools to build, test, and manage
In the directory `tools` there are some Go files to automate most aspects of
building and testing, (hopefully) platform-independent.

The build script is called as:


    go run tools/build/build.go [-v] COMMAND


The flag `-v` enables the verbose mode.
It outputs all commands called by the tool.

Some important `COMMAND`s are:

* `build`: builds the software with correct version information and puts it
  into a freshly created directory `bin`.
* `check`: checks the current state of the working directory to be ready for
  release (or commit).
* `version`: prints the current version information.

Therefore, the easiest way to build your own version of the Zettelstore
software is to execute the command


    go run tools/build/build.go build


In case of errors, please send the output of the verbose execution:


    go run tools/build/build.go -v build


Other tools are:

* `go run tools/clean/clean.go` cleans your Go development workspace.
* `go run tools/check/check.go` executes all linters and unit tests.
  If you add the option `-r` linters are more strict, to be used for a
  release version.
* `go run tools/devtools/devtools.go` install all needed software (see above).
* `go run tools/htmllint/htmllint.go [URL]` checks all generated HTML of a
  Zettelstore accessible at the given URL (default: http://localhost:23123).
* `go run tools/testapi/testapi.go` tests the API against a running
  Zettelstore, which is started automatically.

## A note on the use of Fossil

Zettelstore is managed by the Fossil version control system. Fossil is an
alternative to the ubiquitous Git version control system. However, Go seems to
prefer Git and popular platforms that just support Git.

Some dependencies of Zettelstore, namely [Zettelstore
client](https://t73f.de/r/zsc), [webs](https://t73f.de/r/webs),

[sx](https://t73f.de/r/sx), and [sxwebs](https://t73f.de/r/sxwebs) are also
managed by Fossil. Depending on your development setup, some error messages
might occur.

If the error message mentions an environment variable called `GOVCS` you should
set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous

to `GOVCS=*:all`). Since the Go build system is coupled with Git and some
special platforms, you must allow Go to download a Fossil repository from the
host `zettelstore.de`. The build tool sets `GOVCS` to the right value, but you
may use other `go` commands that try to download a Fossil repository.

On some operating systems, namely Termux on Android, an error message might
state that an user cannot be determined (`cannot determine user`). In this
case, Fossil is allowed to download the repository, but cannot associate it

with an user name. Set the environment variable `USER` to any user name, like:
`USER=nobody go run tools/build.go build`.

Changes to www/changes.wiki.

1
2




















































3
4
























































5
6
7
8
9
10
11
<title>Change Log</title>





















































<a id="0_18"></a>
<h2>Changes for Version 0.18.0 (pending)</h2>

























































<a id="0_17"></a>
<h2>Changes for Version 0.17.0 (2024-03-04)</h2>
  *  Context search operates only on explicit references. Add the directive
     <code>FULL</code> to follow zettel tags additionally.
     (breaking)
  *  Context cost calculation has been changed. Prepare to retrieve different


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<title>Change Log</title>

<a id="0_20"></a>
<h2>Changes for Version 0.20.0 (pending)</h2>
  *  Query aggregates <code>ATOM</code> and <code>RSS</code> are removed, as
     well as the accompanying <code>TITLE</code> query action (parameter).
     Were announced as deprecated in version 0.19.
     (major)

<a id="0_19"></a>
<h2>Changes for Version 0.19.0 (2024-12-13)</h2>
  *  Remove support for renaming zettel, i.e. changing zettel identifier. Was
     announced as deprecated in version 0.18.
     (breaking: api, webui)
  *  Format of zettel identifier will be not changed. The deprecation message
     from version 0.18 is no longer valid.
     (major)
  *  Zettel content for most predefined zettel (ID less or equal than
     0000099999899) is not indexed any more. If you search / query for zettel
     content, these zettel will not be returned. However, their metadata is
     still searchable. Content of predefined zettel with ID between
     00000999999900 and 00000999999999 will be indexed..
     (breaking: api, webui)
  *  Metadata keys <code>superior</code> and <code>subordinate</code> are not
     supported on the WebUI any more. They are still typed as a set of zettel
     identifier, but are treated as ordinary links. Zettel should not form a
     hierarchy, in most cases.
     (major: webui)
  *  Metadata keys <code>sequel</code> and <code>prequel</code> support a
     sequence of zettel. They are supported through the WebUI also.
     <code>sequel</code> is calculated based on <code>prequel</code>. While
     folge zettel are a train of thought, zettel sequences form different train
     of thoughts.
     (major)
  *  Query aggregates <code>ATOM</code> and <code>RSS</code> will be removed in
     the next version of Zettelstore. They were introduced in [#0_7|v0.7] and
     [#0_8|v0.8]. Both are not needed for a digital zettelkasten. Their current
     use is to transform Zettelstore into a blogging engine. RSS and Atom feed
     can be provided by external software, much better than Zettelstore will
     ever do.
     (deprecation)
  *  Fix wrong quote translation for markdown encoder.
     (minor)
  *  Generate <code>&lt;th></code> in table header (was: <code>&lt;td></code>).
     Also applies to SHTML encoder. (minor: webui, api)
  *  External links are now generated in shtml and html with attribute
     rel="external" (previously: class="external").
     (minor: webui, api)
  *  Allow to enable runtime profiling of the software, to be used by
     developers.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_18"></a>
<h2>Changes for Version 0.18.0 (2024-07-11)</h2>
  *  Remove Sx macro <code>defunconst</code>. Use <code>defun</code> instead.
     (breaking: webui)
  *  The sz encoding of zettel does not make use of <code>(SPACE)</code>
     elements any more. Instead, space characters are encoded within the
     <code>(TEXT "...")</code> element. This might affect any client that works
     with the sz encoding to produce some output.
     (breaking)
  *  Format of zettel identifier will be changed in the future to a new format,
     instead of the current timestamp-based format. The usage of zettel
     identifier that are before 1970-01-01T00:00:00 is not allowed any more
     (with the exception of predefined identifier)
     (deprecation)
  *  Due to the planned format of zettel identifier, the &ldquo;rename&rdquo;
     operation is deprecated. It will be removed in version 0.19 or later. If
     you have a significant use case for the rename operation, please contact
     the maintainer immediate.
     (deprecation)
  *  New zettel are now created with the permission for others to read/write
     them. This is important especially for Unix-like systems. If you want the
     previous behaviour, set <code>umask</code> accordingly, for example
     <code>umask 066</code>.
     (major: dirbox)
  *  Add expert-mode zettel &ldquo;Zettelstore Warnings&rdquo; to help
     identifying zettel to upgrade for future migration to planned new zettel
     identifier format.
     (minor: webui)
  *  Add expert-mode zettel &ldquo;Zettelstore Identifier Mapping&rdquo; to
     show a possible mapping from the old identifier format to the new one.
     This should help users to possibly rename some zettel for a metter
     mapping.
     (minor: webui)
  *  Add metadata key <code>created-missing</code> to list zettel without
     stored metadata key <code>created</code>. Needed for migration to planned
     new zettelstore identifier format, which is not based on timestamp of
     zettel creation date.
     (minor)
  *  Add zettel &ldquo;Zettelstore Application Directory&rdquo;, which contains
     identifier for application specific zettel. Needed for planned new
     identifier format.
     (minor: webui)
  *  Update Sx prelude: make macros more robust / more general. This might
     break your code in the future.
     (minor: webui)
  *  Add computed expert-mode zettel &ldquo;Zettelstore Memory&rdquo; with
     zettel identifier <code>00000000000008</code>. It shows some statistics
     about memory usage.
     (minor: webui)
  *  Add computed expert-mode zettel &ldquo;Zettelstore Sx Engine&rdquo; with
     zettel identifier <code>00000000000009</code>. It shows some statistics
     about the internal Sx engine. (Currently only the number of used symbols,
     but this will change in the future.)
     (minor: webui)
  *  Zettelstore client is now Go package t73f.de/r/zsc.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_17"></a>
<h2>Changes for Version 0.17.0 (2024-03-04)</h2>
  *  Context search operates only on explicit references. Add the directive
     <code>FULL</code> to follow zettel tags additionally.
     (breaking)
  *  Context cost calculation has been changed. Prepare to retrieve different
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
     (breaking: webui)
  *  Allow to determine a role zettel for a given role.
     (major: api, webui)
  *  Present user the option to create a (missing) role zettel (in list view).
     Results in a new predefined zettel with identifier 00000000090004, which
     is a template for new role zettel.
     (minor: webui)
  *  Timestamp values can be abbrevated by omitting most of its components.
     Previously, such values that are not in the format YYYYMMDDhhmmss were
     ignored. Now the following formats are also allowed: YYYY, YYYYMM,
     YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm. Querying and sorting work accordingly.
     Previously, only a sequences of zeroes were appended, resulting in illegal
     timestamps, e.g. for YYYY or YYYYMM.
     (minor)
  *  SHTML encoder fixed w.r.t inline quoting. Previously, an &lt;q&gt; tag was







|







172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
     (breaking: webui)
  *  Allow to determine a role zettel for a given role.
     (major: api, webui)
  *  Present user the option to create a (missing) role zettel (in list view).
     Results in a new predefined zettel with identifier 00000000090004, which
     is a template for new role zettel.
     (minor: webui)
  *  Timestamp values can be abbreviated by omitting most of its components.
     Previously, such values that are not in the format YYYYMMDDhhmmss were
     ignored. Now the following formats are also allowed: YYYY, YYYYMM,
     YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm. Querying and sorting work accordingly.
     Previously, only a sequences of zeroes were appended, resulting in illegal
     timestamps, e.g. for YYYY or YYYYMM.
     (minor)
  *  SHTML encoder fixed w.r.t inline quoting. Previously, an &lt;q&gt; tag was
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
     (minor: webui)
  *  ZIP file with manual now contains a zettel 00001000000000 that contains
     its build date (metadata key <code>created</code>) and version (in the
     zettel content)
     (minor)
  *  If an error page cannot be created due to template errors (or similar), a
     plain text error page is delivered instead. It shows the original error
     and the error that occured durng rendering the original error page.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_14"></a>
<h2>Changes for Version 0.14.0 (2023-09-22)</h2>
  *  Remove support for JSON. This was marked deprecated in version 0.12.0. Use







|







220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
     (minor: webui)
  *  ZIP file with manual now contains a zettel 00001000000000 that contains
     its build date (metadata key <code>created</code>) and version (in the
     zettel content)
     (minor)
  *  If an error page cannot be created due to template errors (or similar), a
     plain text error page is delivered instead. It shows the original error
     and the error that occurred during rendering the original error page.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_14"></a>
<h2>Changes for Version 0.14.0 (2023-09-22)</h2>
  *  Remove support for JSON. This was marked deprecated in version 0.12.0. Use
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
  *  Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
     encoding instead.
     (breaking)
  *  Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
     just a plain string, possibly empty. Therefore, no inline formatting (like
     bold text), no links, no footnotes, no citations (the latter made
     rendering the title often questionable, in some contexts). If you used
     special entities, please use the unicode characters directly. However, as
     a good practice, it is often the best to printable ASCII characters.
     (breaking)
  *  Remove runtime configuration <code>marker-external</code>. It was added in
     version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
     change the marker for an external URL, you could modify zettel
     00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
     (Zettelstore User CSS, preferred) by changing / adding a rule to add some







|







380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
  *  Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
     encoding instead.
     (breaking)
  *  Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
     just a plain string, possibly empty. Therefore, no inline formatting (like
     bold text), no links, no footnotes, no citations (the latter made
     rendering the title often questionable, in some contexts). If you used
     special entities, please use the Unicode characters directly. However, as
     a good practice, it is often the best to printable ASCII characters.
     (breaking)
  *  Remove runtime configuration <code>marker-external</code>. It was added in
     version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
     change the marker for an external URL, you could modify zettel
     00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
     (Zettelstore User CSS, preferred) by changing / adding a rule to add some
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
     (minor: api)
  *  Enhance zettel context by raising the importance of folge zettel (and
     similar).
     (minor: api, webui)
  *  Interpret zettel files with extension <code>.webp</code> as an binary
     image file format.
     (minor)
  *  Allow to specify service specific log level via statup configuration and
     via command line.
     (minor)
  *  Allow to specify a zettel to serve footer content via runtime
     comfiguration <code>footer-zettel</code>. Can be overwritten by user
     zettel.
     (minor: webui)
  *  Footer data is automatically separated by a thematic break / horizontal
     rule. If you do not like it, you have to update the base template.
     (minor: webui)
  *  Allow to set runtime configuration <code>home-zettel</code> in the user
     zettel to make it user-specific.
     (minor: webui)
  *  Serve favicon.ico from the asset directory.
     (minor: webui)
  *  Zettelmarkup cheat sheet
     (minor: manual)
  *  Runtime configuration key <code>footer-html</code> will be removed in
     Version 0.10.0. Please use <code>footer-zettel</code> instead.
     (deprecated: webui)
  *  In the next version 0.10.0, the API endpoints for a zettel
     (<code>/j</code>, <code>/p</code>, <code>/v</code>) will be merged with
     endpoint <code>/z</code>. Basically, the previous endpoint will be
     refactored as query parameter of endpoint <code>/z</code>. To reduce
     errors, there will be no version, where the previous endpoint are still
     available and the new funnctionality is still there. This is a warning to
     prepare for some breaking changes in v0.10.0. This also affects the API
     client implementation.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_8"></a>







|



|




















|







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
     (minor: api)
  *  Enhance zettel context by raising the importance of folge zettel (and
     similar).
     (minor: api, webui)
  *  Interpret zettel files with extension <code>.webp</code> as an binary
     image file format.
     (minor)
  *  Allow to specify service specific log level via startup configuration and
     via command line.
     (minor)
  *  Allow to specify a zettel to serve footer content via runtime
     configuration <code>footer-zettel</code>. Can be overwritten by user
     zettel.
     (minor: webui)
  *  Footer data is automatically separated by a thematic break / horizontal
     rule. If you do not like it, you have to update the base template.
     (minor: webui)
  *  Allow to set runtime configuration <code>home-zettel</code> in the user
     zettel to make it user-specific.
     (minor: webui)
  *  Serve favicon.ico from the asset directory.
     (minor: webui)
  *  Zettelmarkup cheat sheet
     (minor: manual)
  *  Runtime configuration key <code>footer-html</code> will be removed in
     Version 0.10.0. Please use <code>footer-zettel</code> instead.
     (deprecated: webui)
  *  In the next version 0.10.0, the API endpoints for a zettel
     (<code>/j</code>, <code>/p</code>, <code>/v</code>) will be merged with
     endpoint <code>/z</code>. Basically, the previous endpoint will be
     refactored as query parameter of endpoint <code>/z</code>. To reduce
     errors, there will be no version, where the previous endpoint are still
     available and the new functionality is still there. This is a warning to
     prepare for some breaking changes in v0.10.0. This also affects the API
     client implementation.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_8"></a>
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
  *  Query results can be ordered for more than one metadata key. Ordering by
     zettel identifier is an implicit last order expression to produce stable
     results.
     (minor: api, webui)
  *  Add support for an asset directory, accessible via URL prefix
     <code>/assests/</code>.
     (minor: server)
  *  Add support for metadata key <code>created</code>, a timestamp when the
     zettel was created. Since key <code>published</code> is now either
     <code>created</code> or <code>modified</code>, it will now always contains
     a valid time stamp.
     (minor)
  *  Add support for metadata key <code>author</code>. It will be displayed on
     a zettel, if set.
     (minor: webui)
  *  Remove CSS for lists. The browsers default value for
     <code>padding-left</code> will be used.
     (minor: webui)
  *  Removed templates for rendering roles and tags lists. This is now done by
     query actions.
     (minor: webui)
  *  Tags within zettel content are deprecated in version 0.8. This affects the
     computed metadata keys <code>content-tags</code> and
     <code>all-tags</code>. They will be removed. The number sign of a content
     tag introduces unintended tags, esp. in the english language; content tags
     may occur within links &rarr; links within links, when rendered as HTML;
     content tags may occur in the title of a zettel; naming of content tags,
     zettel tags, and their union is confusing for many. Migration: use zettel
     tags or replace content tag with a search.
     (deprecated: zettelmarkup)
  *  Cleanup names for HTTP query parameter for API calls. Essentially,
     underscore characters in front are removed. Please use new names, old







|
















|







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
  *  Query results can be ordered for more than one metadata key. Ordering by
     zettel identifier is an implicit last order expression to produce stable
     results.
     (minor: api, webui)
  *  Add support for an asset directory, accessible via URL prefix
     <code>/assests/</code>.
     (minor: server)
  *  Add support for metadata key <code>created</code>, a time stamp when the
     zettel was created. Since key <code>published</code> is now either
     <code>created</code> or <code>modified</code>, it will now always contains
     a valid time stamp.
     (minor)
  *  Add support for metadata key <code>author</code>. It will be displayed on
     a zettel, if set.
     (minor: webui)
  *  Remove CSS for lists. The browsers default value for
     <code>padding-left</code> will be used.
     (minor: webui)
  *  Removed templates for rendering roles and tags lists. This is now done by
     query actions.
     (minor: webui)
  *  Tags within zettel content are deprecated in version 0.8. This affects the
     computed metadata keys <code>content-tags</code> and
     <code>all-tags</code>. They will be removed. The number sign of a content
     tag introduces unintended tags, esp. in the English language; content tags
     may occur within links &rarr; links within links, when rendered as HTML;
     content tags may occur in the title of a zettel; naming of content tags,
     zettel tags, and their union is confusing for many. Migration: use zettel
     tags or replace content tag with a search.
     (deprecated: zettelmarkup)
  *  Cleanup names for HTTP query parameter for API calls. Essentially,
     underscore characters in front are removed. Please use new names, old
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
     (bug)

<h2>Changes for Version 0.6.0 (2022-08-11)</h2>
  *  Translating of "..." into horizontal ellipsis is no longer supported. Use
     &amp;hellip; instead.
     (breaking: zettelmarkup)
  *  Allow to specify search expressions, which allow to specify search
     criterias by using a simple syntax. Can be specified in WebUI's search box
     and via the API by using query parameter "_s".
     (major: api, webui)
  *  A link reference is allowed to be a search expression. The WebUI will
     render this as a link to a list of zettel that satisfy the search
     expression.
     (major: zettelmarkup, webui)
  *  A block transclusion is allowed to specify a search expression. When
     evaluated, the transclusion is replaced by a list of zettel that satisfy
     the search expression.
     (major: zettelmarkup)
  *  When presenting a zettel list, allow to change the search expression.
     (minor: webui)
  *  When evaluating a zettel, ignore transclusions if current user is not
     allowed to read transcluded zettel.
     (minor)
  *  Added a small tutorial for Zettelmarkup.
     (minor: manual)
  *  Using URL query parameter to search for metdata values, specify an
     ordering, an offset, and a limit for the resulting list, will be removed
     in version 0.7. Replace these with the more useable search expressions.
     Please be aware that the = search operator is also deprecated. It was only
     introduced to help the migration.
     (deprecated: api, webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.







|

















|







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
     (bug)

<h2>Changes for Version 0.6.0 (2022-08-11)</h2>
  *  Translating of "..." into horizontal ellipsis is no longer supported. Use
     &amp;hellip; instead.
     (breaking: zettelmarkup)
  *  Allow to specify search expressions, which allow to specify search
     criteria by using a simple syntax. Can be specified in WebUI's search box
     and via the API by using query parameter "_s".
     (major: api, webui)
  *  A link reference is allowed to be a search expression. The WebUI will
     render this as a link to a list of zettel that satisfy the search
     expression.
     (major: zettelmarkup, webui)
  *  A block transclusion is allowed to specify a search expression. When
     evaluated, the transclusion is replaced by a list of zettel that satisfy
     the search expression.
     (major: zettelmarkup)
  *  When presenting a zettel list, allow to change the search expression.
     (minor: webui)
  *  When evaluating a zettel, ignore transclusions if current user is not
     allowed to read transcluded zettel.
     (minor)
  *  Added a small tutorial for Zettelmarkup.
     (minor: manual)
  *  Using URL query parameter to search for metadata values, specify an
     ordering, an offset, and a limit for the resulting list, will be removed
     in version 0.7. Replace these with the more useable search expressions.
     Please be aware that the = search operator is also deprecated. It was only
     introduced to help the migration.
     (deprecated: api, webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.
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
     Zettelstore WebUI.
     (minor: webui)
  *  A zettel can be saved while creating / editing it. There is no need to
     manually re-edit it by using the 'e' endpoint.
     (minor: webui)
  *  Zettel role and zettel syntax are backed by a HTML5 data list element
     which lists supported and used values to help to enter a valid value.
     (mirnor: webui)
  *  Allow to use startup configuration, even if started in simple mode.
     (minor)
  *  Log authentication issues in level "sense"; add caller IP address to some
     web server log messages.
     (minor: web server)
  *  New startup configuration key <kbd>max-request-size</kbd> to limit a web
     request body to prevent client sending too large requests.
     (minor: web server)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_4"></a>
<h2>Changes for Version 0.4 (2022-03-08)</h2>
  *  Encoding &ldquo;djson&rdquo; renamed to &ldquo;zjson&rdquo; (<em>zettel
     json</em>).
     (breaking: api; minor: webui)
  *  Remove inline quotation syntax <code>&lt;&lt;...&lt;&lt;</code>. Now,
     <code>&quot;&quot;...&quot;&quot;</code> generates the equivalent code.
     Typographical quotes are generated by the browser, not by Zettelstore.
     (breaking: Zettelmarkup)
  *  Remove inline formatting for monospace. Its syntax is now used by the
     similar syntax element of literal computer input. Monospace was just
     a visual element with no semantic association. Now, the syntax
     <kbd>++...++</kbd> is obsolete.
     (breaking: Zettelmarkup).
  *  Remove API call to parse Zettelmarkup texts and encode it as text and
     HTML. Was call &ldquo;POST /v&rdquo;. It was needed to separately encode
     the titles of zettel. The same effect can be achieved by fetching the
     ZJSON representation and encode it using the function in the Zettelstore







|




















|
|







713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
     Zettelstore WebUI.
     (minor: webui)
  *  A zettel can be saved while creating / editing it. There is no need to
     manually re-edit it by using the 'e' endpoint.
     (minor: webui)
  *  Zettel role and zettel syntax are backed by a HTML5 data list element
     which lists supported and used values to help to enter a valid value.
     (minor: webui)
  *  Allow to use startup configuration, even if started in simple mode.
     (minor)
  *  Log authentication issues in level "sense"; add caller IP address to some
     web server log messages.
     (minor: web server)
  *  New startup configuration key <kbd>max-request-size</kbd> to limit a web
     request body to prevent client sending too large requests.
     (minor: web server)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_4"></a>
<h2>Changes for Version 0.4 (2022-03-08)</h2>
  *  Encoding &ldquo;djson&rdquo; renamed to &ldquo;zjson&rdquo; (<em>zettel
     json</em>).
     (breaking: api; minor: webui)
  *  Remove inline quotation syntax <code>&lt;&lt;...&lt;&lt;</code>. Now,
     <code>&quot;&quot;...&quot;&quot;</code> generates the equivalent code.
     Typographical quotes are generated by the browser, not by Zettelstore.
     (breaking: Zettelmarkup)
  *  Remove inline formatting for mono space. Its syntax is now used by the
     similar syntax element of literal computer input. Mono space was just
     a visual element with no semantic association. Now, the syntax
     <kbd>++...++</kbd> is obsolete.
     (breaking: Zettelmarkup).
  *  Remove API call to parse Zettelmarkup texts and encode it as text and
     HTML. Was call &ldquo;POST /v&rdquo;. It was needed to separately encode
     the titles of zettel. The same effect can be achieved by fetching the
     ZJSON representation and encode it using the function in the Zettelstore
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
     a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel
     identifier.
     (minor: api, webui)
  *  Change generated URLs for zettel-creation forms. If you have bookmarked
     them, e.g. to create a new zettel, you should update.
     (minor: webui)
  *  Remove support for metadata key <code>no-index</code> to suppress indexing
     selected zettel. It was introduced in <a href="#0_0_11">v0.0.11</a>, but
     disallows some future optimizations for searching zettel.
     (minor: api, webui)
  *  Make some metadata-based searches a little bit faster by executing
     a (in-memory-based) full-text search first. Now only those zettel are
     loaded from file that contain the metdata value.
     (minor: api, webui)
  *  Add an API call to retrieve the version of the Zettelstore.
     (minor: api)
  *  Limit the amount of zettel and bytes to be stored in a memory box. Allows
     to use it with public access.
     (minor: box)
  *  Disallow to cache the authentication cookie. Will remove most unexpected
     log-outs when using a mobile device.
     (minor: webui)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_3"></a>
<h2>Changes for Version 0.3 (2022-02-09)</h2>
  *  Zettel files with extension <code>.meta</code> are now treated as content
     files. Previoulsy, they were interpreted as metadata files. The
     interpretation as metadata files was deprecated in version 0.2.
     (breaking: directory and file/zip box)
  *  Add syntax &ldquo;draw&rdquo; to produce some graphical representations.
     (major)
  *  Add Zettelmarkup syntax to specify full transclusion of other zettel.
     (major: Zettelmarkup)
  *  Add Zettelmarkup syntax to specify inline-zettel, both for







|
|



|















|







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
     a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel
     identifier.
     (minor: api, webui)
  *  Change generated URLs for zettel-creation forms. If you have bookmarked
     them, e.g. to create a new zettel, you should update.
     (minor: webui)
  *  Remove support for metadata key <code>no-index</code> to suppress indexing
     selected zettel. It was introduced in [#0_0_11|v0.0.11], but disallows
     some future optimizations for searching zettel.
     (minor: api, webui)
  *  Make some metadata-based searches a little bit faster by executing
     a (in-memory-based) full-text search first. Now only those zettel are
     loaded from file that contain the metadata value.
     (minor: api, webui)
  *  Add an API call to retrieve the version of the Zettelstore.
     (minor: api)
  *  Limit the amount of zettel and bytes to be stored in a memory box. Allows
     to use it with public access.
     (minor: box)
  *  Disallow to cache the authentication cookie. Will remove most unexpected
     log-outs when using a mobile device.
     (minor: webui)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_3"></a>
<h2>Changes for Version 0.3 (2022-02-09)</h2>
  *  Zettel files with extension <code>.meta</code> are now treated as content
     files. Previously, they were interpreted as metadata files. The
     interpretation as metadata files was deprecated in version 0.2.
     (breaking: directory and file/zip box)
  *  Add syntax &ldquo;draw&rdquo; to produce some graphical representations.
     (major)
  *  Add Zettelmarkup syntax to specify full transclusion of other zettel.
     (major: Zettelmarkup)
  *  Add Zettelmarkup syntax to specify inline-zettel, both for
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
     (minor)
  *  Add computed zettel that lists all supported parser / recognized zettel
     syntaxes.
     (minor)
  *  Add API call to check for enabled authentication.
     (minor: api)
  *  Renewing an API access token works even if authentication is not enabled.
     This corresponds to the behaviour of optaining an access token.
     (minor: api)
  *  If there is nothing to return, use HTTP status code 204, instead of 200 +
     <code>Content-Length: 0</code>.
     (minor: api)
  *  Metadata key <code>duplicates</code> stores the duplicate file names,
     instead of just a boolean value that there were duplicate file names.
     (minor)
  *  Document autostarting Zettelstore on Windows, macOS, and Linux.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_1"></a><a id="0_1_0"></a>
<h2>Changes for Version 0.1 (2021-11-11)</h2>
  *  v0.1.3 (2021-12-15) fixes a bug where the modification date could be set







|







|







857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
     (minor)
  *  Add computed zettel that lists all supported parser / recognized zettel
     syntaxes.
     (minor)
  *  Add API call to check for enabled authentication.
     (minor: api)
  *  Renewing an API access token works even if authentication is not enabled.
     This corresponds to the behaviour of obtaining an access token.
     (minor: api)
  *  If there is nothing to return, use HTTP status code 204, instead of 200 +
     <code>Content-Length: 0</code>.
     (minor: api)
  *  Metadata key <code>duplicates</code> stores the duplicate file names,
     instead of just a boolean value that there were duplicate file names.
     (minor)
  *  Document auto starting Zettelstore on Windows, macOS, and Linux.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_1"></a><a id="0_1_0"></a>
<h2>Changes for Version 0.1 (2021-11-11)</h2>
  *  v0.1.3 (2021-12-15) fixes a bug where the modification date could be set
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
     syntax will not be supported. The reason is the collision with URLs that
     also contain the characters <code>//</code>. The ZMK encoding of a zettel
     may help with the transition
     (<code>/v/{ZettelID}?_part=zettel&amp;_enc=zmk</code>, on the Info page of
     each zettel in the WebUI). Additionally, all deprecated uses of
     <code>//</code> will be rendered with a dashed box within the WebUI.
     (breaking: Zettelmarkup).
  *  API client software is now a [https://zettelstore.de/client/|separate]
     project.
     (breaking)
  *  Initial support for HTTP security headers (Content-Security-Policy,
     Permissions-Policy, Referrer-Policy, X-Content-Type-Options,
     X-Frame-Options). Header values are currently some constant values.
     (possibly breaking: api, webui)
  *  Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup
     (strong, delete) is still allowed and replaces the visual elements
     syntactically. The visual appearance should not change (depends on your
     changes / additions to CSS zettel).
     (possibly breaking: Zettelmarkup).
  *  Add API endpoint <code>POST /v</code> to retrieve HTMl and text encoded
     strings from given ZettelMarkup encoded values. This will be used to
     render a HTML page from a given zettel: in many cases the title of







|
<





|







892
893
894
895
896
897
898
899

900
901
902
903
904
905
906
907
908
909
910
911
912
     syntax will not be supported. The reason is the collision with URLs that
     also contain the characters <code>//</code>. The ZMK encoding of a zettel
     may help with the transition
     (<code>/v/{ZettelID}?_part=zettel&amp;_enc=zmk</code>, on the Info page of
     each zettel in the WebUI). Additionally, all deprecated uses of
     <code>//</code> will be rendered with a dashed box within the WebUI.
     (breaking: Zettelmarkup).
  *  API client software is now a 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, strike through). Semantic Zettelmarkup
     (strong, delete) is still allowed and replaces the visual elements
     syntactically. The visual appearance should not change (depends on your
     changes / additions to CSS zettel).
     (possibly breaking: Zettelmarkup).
  *  Add API endpoint <code>POST /v</code> to retrieve HTMl and text encoded
     strings from given ZettelMarkup encoded values. This will be used to
     render a HTML page from a given zettel: in many cases the title of
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
     translated to their lower case equivalent before comparing them and when
     you edit a zettel through Zettelstore. If you just modify the zettel
     files, your tag values remain unchanged.
     (major; breaking)
  *  Endpoint <code>/z/{ID}</code> allows the same methods as endpoint
     <code>/j/{ID}</code>: <code>GET</code> retrieves zettel (see above),
     <code>PUT</code> updates a zettel, <code>DELETE</code> deletes a zettel,
     <code>MOVE</code> renames a zettel. In addtion, <code>POST /z</code> will
     create a new zettel. When zettel data must be given, the format is plain
     text, with metadata separated from content by an empty line. See
     documentation for more details.
     (major: api (plus WebUI for some details))
  *  Allows to transclude / expand the content of another zettel into a target
     zettel when the zettel is rendered. By using the syntax of embedding an
     image (which is some kind of expansion too), the first top-level paragraph







|







965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
     translated to their lower case equivalent before comparing them and when
     you edit a zettel through Zettelstore. If you just modify the zettel
     files, your tag values remain unchanged.
     (major; breaking)
  *  Endpoint <code>/z/{ID}</code> allows the same methods as endpoint
     <code>/j/{ID}</code>: <code>GET</code> retrieves zettel (see above),
     <code>PUT</code> updates a zettel, <code>DELETE</code> deletes a zettel,
     <code>MOVE</code> renames a zettel. In addition, <code>POST /z</code> will
     create a new zettel. When zettel data must be given, the format is plain
     text, with metadata separated from content by an empty line. See
     documentation for more details.
     (major: api (plus WebUI for some details))
  *  Allows to transclude / expand the content of another zettel into a target
     zettel when the zettel is rendered. By using the syntax of embedding an
     image (which is some kind of expansion too), the first top-level paragraph
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
     above suffixes, but as a string type)
  *  New <code>user-role</code> &ldquo;creator&rdquo;, which is only allowed to
     create new zettel (except user zettel). This role may only read and update
     public zettel or its own user zettel. Added to support future client
     software (e.g. on a mobile device) that automatically creates new zettel
     but, in case of a password loss, should not allow to read existing zettel.
     (minor, possibly breaking, because new zettel template zettel must always
     prepend the string <code>new-</code> before metdata keys that should be
     transferred to the new zettel)
  *  New suported metadata key <code>box-number</code>, which gives an
     indication from which box the zettel was loaded.
     (minor)
  *  New supported syntax <code>html</code>.
     (minor)
  *  New predefined zettel &ldquo;User CSS&rdquo; that can be used to redefine
     some predefined CSS (without modifying the base CSS zettel).
     (minor: webui)







|

|







1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
     above suffixes, but as a string type)
  *  New <code>user-role</code> &ldquo;creator&rdquo;, which is only allowed to
     create new zettel (except user zettel). This role may only read and update
     public zettel or its own user zettel. Added to support future client
     software (e.g. on a mobile device) that automatically creates new zettel
     but, in case of a password loss, should not allow to read existing zettel.
     (minor, possibly breaking, because new zettel template zettel must always
     prepend the string <code>new-</code> before metadata keys that should be
     transferred to the new zettel)
  *  New supported metadata key <code>box-number</code>, which gives an
     indication from which box the zettel was loaded.
     (minor)
  *  New supported syntax <code>html</code>.
     (minor)
  *  New predefined zettel &ldquo;User CSS&rdquo; that can be used to redefine
     some predefined CSS (without modifying the base CSS zettel).
     (minor: webui)
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
  #  You update the per-process limit of open files on macOS.
  #  You setup a virtualization environment to run Zettelstore on Linux or
     Windows.
  #  You wait for version 0.0.12 which addresses this issue.

<a id="0_0_10"></a>
<h2>Changes for Version 0.0.10 (2021-02-26)</h2>
  *  Menu item &ldquo;Home&rdquo; now redirects to a home zettel.
     Its default identifier is <code>000100000000</code>. The identifier can be







|







1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
  #  You update the per-process limit of open files on macOS.
  #  You setup a virtualisation environment to run Zettelstore on Linux or
     Windows.
  #  You wait for version 0.0.12 which addresses this issue.

<a id="0_0_10"></a>
<h2>Changes for Version 0.0.10 (2021-02-26)</h2>
  *  Menu item &ldquo;Home&rdquo; now redirects to a home zettel.
     Its default identifier is <code>000100000000</code>. The identifier can be
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230

1231
1232
1233
1234
1235
1236
1237
  *  (bug)   Fixes a memory leak that results in too many open files after
             approx. 125 reload operations.
  *  (major) Predefined templates for new zettel got an explicit value for
             visibility: &ldquo;login&rdquo;. Please update these zettel if you
             modified them.
  *  (major) Rename key <code>readonly</code> of <i>Zettelstore Startup
             Configuration</i> to <code>read-only-mode</code>. This was done to
             avoid some confusion with the the zettel metadata key
             <code>read-only</code>. <b>Please adapt your startup configuration.
             Otherwise your Zettelstore will be accidentally writable.</b>

  *  (minor) References starting with &ldquo;./&rdquo; and &ldquo;../&rdquo;
             are treated as a local reference. Previously, only the prefix
             &ldquo;/&rdquo; was treated as a local reference.
  *  (major) Metadata key <code>modified</code> will be set automatically to
             the current local time if a zettel is updated through Zettelstore.
             <b>If you used that key previously for your own, you should rename
             it before you upgrade.</b>







|
|
|
>







1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
  *  (bug)   Fixes a memory leak that results in too many open files after
             approx. 125 reload operations.
  *  (major) Predefined templates for new zettel got an explicit value for
             visibility: &ldquo;login&rdquo;. Please update these zettel if you
             modified them.
  *  (major) Rename key <code>readonly</code> of <i>Zettelstore Startup
             Configuration</i> to <code>read-only-mode</code>. This was done to
             avoid some confusion with the zettel metadata key
             <code>read-only</code>. <b>Please adapt your startup
             configuration. Otherwise your Zettelstore will be accidentally
             writable.</b>
  *  (minor) References starting with &ldquo;./&rdquo; and &ldquo;../&rdquo;
             are treated as a local reference. Previously, only the prefix
             &ldquo;/&rdquo; was treated as a local reference.
  *  (major) Metadata key <code>modified</code> will be set automatically to
             the current local time if a zettel is updated through Zettelstore.
             <b>If you used that key previously for your own, you should rename
             it before you upgrade.</b>

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
<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.17.0</code> (2024-03-04).

  *  [/uv/zettelstore-0.17.0-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.17.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.17.0-darwin-arm64.zip|macOS] (arm64)
  *  [/uv/zettelstore-0.17.0-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.17.0-windows-amd64.zip|Windows] (amd64)

Unzip the appropriate file, install and execute Zettelstore according to the manual.

<h2>Zettel for the manual</h2>
As a starter, you can download the zettel for the manual
[/uv/manual-0.17.0.zip|here].
Just unzip the contained files and put them into your zettel folder or
configure a file box to read the zettel directly from the ZIP file.











|

|
|
|
|
|





|


1
2
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.19.0</code> (2024-12-13).

  *  [/uv/zettelstore-0.19.0-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.19.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.19.0-darwin-arm64.zip|macOS] (arm64)
  *  [/uv/zettelstore-0.19.0-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.19.0-windows-amd64.zip|Windows] (amd64)

Unzip the appropriate file, install and execute Zettelstore according to the manual.

<h2>Zettel for the manual</h2>
As a starter, you can download the zettel for the manual
[/uv/manual-0.19.0.zip|here].
Just unzip the contained files and put them into your zettel folder or
configure a file box to read the zettel directly from the ZIP file.

Changes to www/impri.wiki.

10
11
12
13
14
15
16
17
18
If you do not log into this site, or login as the user &quot;anonymous&quot;,
the only personal data this web service will process is your IP adress. It will
be used to send the data of the website you requested to you and to mitigate
possible attacks against this website.

This website is hosted by [https://ionos.de|1&amp;1 IONOS SE].
According to
[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-11-ionos-produktes/andere-11-ionos-produkte/|their information],
no processing of personal data is done by them.







|

10
11
12
13
14
15
16
17
18
If you do not log into this site, or login as the user &quot;anonymous&quot;,
the only personal data this web service will process is your IP adress. It will
be used to send the data of the website you requested to you and to mitigate
possible attacks against this website.

This website is hosted by [https://ionos.de|1&amp;1 IONOS SE].
According to
[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-ionos-produktes/andere-ionos-produkte/|their information],
no processing of personal data is done by them.

Changes to www/index.wiki.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
To get an initial impression, take a look at the
[https://zettelstore.de/manual/|manual]. It is a live example of the
zettelstore software, running in read-only mode.

The software, including the manual, is licensed under the
[/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)].

  *  [https://zettelstore.de/client|Zettelstore Client] provides client
     software to access Zettelstore via its API more easily.
  *  [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
     software, which often connects to Zettelstore via its API. Some of the
     software packages may be experimental.
  *  [https://zettelstore.de/sx|Sx] provides an evaluator for symbolic
     expressions, which is unsed for HTML templates and more.

[https://mastodon.social/tags/Zettelstore|Stay tuned]&nbsp;&hellip;
<hr>
<h3>Latest Release: 0.17.0 (2024-03-04)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_17|Change summary]
  *  [/timeline?p=v0.17.0&bt=v0.16.0&y=ci|Check-ins for version 0.17.0],
     [/vdiff?to=v0.17.0&from=v0.16.0|content diff]
  *  [/timeline?df=v0.17.0&y=ci|Check-ins derived from the 0.17.0 release],
     [/vdiff?from=v0.17.0&to=trunk|content diff]
  *  [./plan.wiki|Limitations and planned improvements]
  *  [/timeline?t=release|Timeline of all past releases]

<hr>
<h2>Build instructions</h2>

Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read
the [./build.md|instructions] for details.

  *  [/dir?ci=trunk|Source code]
  *  [/download|Download the source code] as a tarball or a ZIP file
     (you must [/login|login] as user &quot;anonymous&quot;).







|
|



|
|



|

|
|
|
|
|





>






12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
To get an initial impression, take a look at the
[https://zettelstore.de/manual/|manual]. It is a live example of the
zettelstore software, running in read-only mode.

The software, including the manual, is licensed under the
[/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)].

  *  [https://t73f.de/r/zsc|Zettelstore Client] provides client software to
     access Zettelstore via its API more easily.
  *  [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
     software, which often connects to Zettelstore via its API. Some of the
     software packages may be experimental.
  *  [https://t73f.de/r/sx|Sx] provides an evaluator for symbolic
     expressions, which is used for HTML templates and more.

[https://mastodon.social/tags/Zettelstore|Stay tuned]&nbsp;&hellip;
<hr>
<h3>Latest Release: 0.19.0 (2024-12-13)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_19|Change summary]
  *  [/timeline?p=v0.19.0&bt=v0.18.0&y=ci|Check-ins for version 0.19],
     [/vdiff?to=v0.19.0&from=v0.18.0|content diff]
  *  [/timeline?df=v0.19.0&y=ci|Check-ins derived from the 0.19 release],
     [/vdiff?from=v0.19.0&to=trunk|content diff]
  *  [./plan.wiki|Limitations and planned improvements]
  *  [/timeline?t=release|Timeline of all past releases]

<hr>
<h2>Build instructions</h2>

Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read
the [./build.md|instructions] for details.

  *  [/dir?ci=trunk|Source code]
  *  [/download|Download the source code] as a tarball or a ZIP file
     (you must [/login|login] as user &quot;anonymous&quot;).

Changes to zettel/content.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"bytes"
	"encoding/base64"
	"errors"
	"io"
	"unicode"
	"unicode/utf8"

	"zettelstore.de/client.fossil/input"
)

// Content is just the content of a zettel.
type Content struct {
	data     []byte
	isBinary bool
}







|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"bytes"
	"encoding/base64"
	"errors"
	"io"
	"unicode"
	"unicode/utf8"

	"t73f.de/r/zsc/input"
)

// Content is just the content of a zettel.
type Content struct {
	data     []byte
	isBinary bool
}
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
	pos := inp.Pos
	for inp.Ch != input.EOS {
		if input.IsEOLEOS(inp.Ch) {
			inp.Next()
			pos = inp.Pos
			continue
		}
		if !input.IsSpace(inp.Ch) {
			break
		}
		inp.Next()
	}
	zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace)
}








|







73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
	pos := inp.Pos
	for inp.Ch != input.EOS {
		if input.IsEOLEOS(inp.Ch) {
			inp.Next()
			pos = inp.Pos
			continue
		}
		if !inp.IsSpace() {
			break
		}
		inp.Next()
	}
	zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace)
}

Changes to zettel/id/digraph.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"maps"
	"slices"
)

// Digraph relates zettel identifier in a directional way.
type Digraph map[Zid]Set

// AddVertex adds an edge / vertex to the digraph.
func (dg Digraph) AddVertex(zid Zid) Digraph {
	if dg == nil {
		return Digraph{zid: nil}
	}
	if _, found := dg[zid]; !found {







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"maps"
	"slices"
)

// Digraph relates zettel identifier in a directional way.
type Digraph map[Zid]*Set

// AddVertex adds an edge / vertex to the digraph.
func (dg Digraph) AddVertex(zid Zid) Digraph {
	if dg == nil {
		return Digraph{zid: nil}
	}
	if _, found := dg[zid]; !found {
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
	}
}

// AddEdge adds a connection from `zid1` to `zid2`.
// Both vertices must be added before. Otherwise the function may panic.
func (dg Digraph) AddEdge(fromZid, toZid Zid) Digraph {
	if dg == nil {
		return Digraph{fromZid: Set(nil).Add(toZid), toZid: nil}
	}
	dg[fromZid] = dg[fromZid].Add(toZid)
	return dg
}

// AddEgdes adds all given `Edge`s to the digraph.
//







|







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
	}
}

// AddEdge adds a connection from `zid1` to `zid2`.
// Both vertices must be added before. Otherwise the function may panic.
func (dg Digraph) AddEdge(fromZid, toZid Zid) Digraph {
	if dg == nil {
		return Digraph{fromZid: (*Set)(nil).Add(toZid), toZid: nil}
	}
	dg[fromZid] = dg[fromZid].Add(toZid)
	return dg
}

// AddEgdes adds all given `Edge`s to the digraph.
//
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
		dg = dg.AddEdge(edge.From, edge.To)
	}
	return dg
}

// Equal returns true if both digraphs have the same vertices and edges.
func (dg Digraph) Equal(other Digraph) bool {
	return maps.EqualFunc(dg, other, func(cg, co Set) bool { return cg.Equal(co) })
}

// Clone a digraph.
func (dg Digraph) Clone() Digraph {
	if len(dg) == 0 {
		return nil
	}







|







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
		dg = dg.AddEdge(edge.From, edge.To)
	}
	return dg
}

// Equal returns true if both digraphs have the same vertices and edges.
func (dg Digraph) Equal(other Digraph) bool {
	return maps.EqualFunc(dg, other, func(cg, co *Set) bool { return cg.Equal(co) })
}

// Clone a digraph.
func (dg Digraph) Clone() Digraph {
	if len(dg) == 0 {
		return nil
	}
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
		return false
	}
	_, found := dg[zid]
	return found
}

// Vertices returns the set of all vertices.
func (dg Digraph) Vertices() Set {
	if len(dg) == 0 {
		return nil
	}
	verts := NewSetCap(len(dg))
	for vert := range dg {
		verts.Add(vert)
	}
	return verts
}

// Edges returns an unsorted slice of the edges of the digraph.
func (dg Digraph) Edges() (es EdgeSlice) {
	for vert, closure := range dg {
		for next := range closure {
			es = append(es, Edge{From: vert, To: next})
		}
	}
	return es
}

// Originators will return the set of all vertices that are not referenced
// a the to-part of an edge.
func (dg Digraph) Originators() Set {
	if len(dg) == 0 {
		return nil
	}
	origs := dg.Vertices()
	for _, closure := range dg {
		origs.Substract(closure)
	}
	return origs
}

// Terminators returns the set of all vertices that does not reference
// other vertices.
func (dg Digraph) Terminators() (terms Set) {
	for vert, closure := range dg {
		if len(closure) == 0 {
			terms = terms.Add(vert)
		}
	}
	return terms
}

// TransitiveClosure calculates the sub-graph that is reachable from `zid`.
func (dg Digraph) TransitiveClosure(zid Zid) (tc Digraph) {
	if len(dg) == 0 {
		return nil
	}
	var marked Set
	stack := Slice{zid}
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr) {
			continue
		}
		tc = tc.AddVertex(curr)
		for next := range dg[curr] {
			tc = tc.AddVertex(next)
			tc = tc.AddEdge(curr, next)
			stack = append(stack, next)
		}
		marked = marked.Add(curr)
	}
	return tc
}

// ReachableVertices calculates the set of all vertices that are reachable
// from the given `zid`.
func (dg Digraph) ReachableVertices(zid Zid) (tc Set) {
	if len(dg) == 0 {
		return nil
	}
	stack := dg[zid].Sorted()
	for last := len(stack) - 1; last >= 0; last = len(stack) - 1 {
		curr := stack[last]
		stack = stack[:last]
		if tc.Contains(curr) {
			continue
		}
		closure, found := dg[curr]
		if !found {
			continue
		}
		tc = tc.Add(curr)
		for next := range closure {
			stack = append(stack, next)
		}
	}
	return tc
}

// IsDAG returns a vertex and false, if the graph has a cycle containing the vertex.
func (dg Digraph) IsDAG() (Zid, bool) {
	for vertex := range dg {
		if dg.ReachableVertices(vertex).Contains(vertex) {
			return vertex, false
		}
	}
	return Invalid, true
}

// Reverse returns a graph with reversed edges.
func (dg Digraph) Reverse() (revDg Digraph) {
	for vertex, closure := range dg {
		revDg = revDg.AddVertex(vertex)
		for next := range closure {
			revDg = revDg.AddVertex(next)
			revDg = revDg.AddEdge(next, vertex)
		}
	}
	return revDg
}

// SortReverse returns a deterministic, topological, reverse sort of the
// digraph.
//
// Works only if digraph is a DAG. Otherwise the algorithm will not terminate
// or returns an arbitrary value.
func (dg Digraph) SortReverse() (sl Slice) {
	if len(dg) == 0 {
		return nil
	}
	tempDg := dg.Clone()
	for len(tempDg) > 0 {
		terms := tempDg.Terminators()
		if len(terms) == 0 {
			break
		}
		termSlice := terms.Sorted()
		slices.Reverse(termSlice)
		sl = append(sl, termSlice...)
		for t := range terms {
			tempDg.RemoveVertex(t)
		}
	}
	return sl
}







|













|

|






|





|






|

|











|








|



|







|



|











|

|


















|


|
















|


|


|

|



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
		return false
	}
	_, found := dg[zid]
	return found
}

// Vertices returns the set of all vertices.
func (dg Digraph) Vertices() *Set {
	if len(dg) == 0 {
		return nil
	}
	verts := NewSetCap(len(dg))
	for vert := range dg {
		verts.Add(vert)
	}
	return verts
}

// Edges returns an unsorted slice of the edges of the digraph.
func (dg Digraph) Edges() (es EdgeSlice) {
	for vert, closure := range dg {
		closure.ForEach(func(next Zid) {
			es = append(es, Edge{From: vert, To: next})
		})
	}
	return es
}

// Originators will return the set of all vertices that are not referenced
// a the to-part of an edge.
func (dg Digraph) Originators() *Set {
	if len(dg) == 0 {
		return nil
	}
	origs := dg.Vertices()
	for _, closure := range dg {
		origs.ISubstract(closure)
	}
	return origs
}

// Terminators returns the set of all vertices that does not reference
// other vertices.
func (dg Digraph) Terminators() (terms *Set) {
	for vert, closure := range dg {
		if closure.IsEmpty() {
			terms = terms.Add(vert)
		}
	}
	return terms
}

// TransitiveClosure calculates the sub-graph that is reachable from `zid`.
func (dg Digraph) TransitiveClosure(zid Zid) (tc Digraph) {
	if len(dg) == 0 {
		return nil
	}
	var marked *Set
	stack := Slice{zid}
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr) {
			continue
		}
		tc = tc.AddVertex(curr)
		dg[curr].ForEach(func(next Zid) {
			tc = tc.AddVertex(next)
			tc = tc.AddEdge(curr, next)
			stack = append(stack, next)
		})
		marked = marked.Add(curr)
	}
	return tc
}

// ReachableVertices calculates the set of all vertices that are reachable
// from the given `zid`.
func (dg Digraph) ReachableVertices(zid Zid) (tc *Set) {
	if len(dg) == 0 {
		return nil
	}
	stack := dg[zid].SafeSorted()
	for last := len(stack) - 1; last >= 0; last = len(stack) - 1 {
		curr := stack[last]
		stack = stack[:last]
		if tc.Contains(curr) {
			continue
		}
		closure, found := dg[curr]
		if !found {
			continue
		}
		tc = tc.Add(curr)
		closure.ForEach(func(next Zid) {
			stack = append(stack, next)
		})
	}
	return tc
}

// IsDAG returns a vertex and false, if the graph has a cycle containing the vertex.
func (dg Digraph) IsDAG() (Zid, bool) {
	for vertex := range dg {
		if dg.ReachableVertices(vertex).Contains(vertex) {
			return vertex, false
		}
	}
	return Invalid, true
}

// Reverse returns a graph with reversed edges.
func (dg Digraph) Reverse() (revDg Digraph) {
	for vertex, closure := range dg {
		revDg = revDg.AddVertex(vertex)
		closure.ForEach(func(next Zid) {
			revDg = revDg.AddVertex(next)
			revDg = revDg.AddEdge(next, vertex)
		})
	}
	return revDg
}

// SortReverse returns a deterministic, topological, reverse sort of the
// digraph.
//
// Works only if digraph is a DAG. Otherwise the algorithm will not terminate
// or returns an arbitrary value.
func (dg Digraph) SortReverse() (sl Slice) {
	if len(dg) == 0 {
		return nil
	}
	tempDg := dg.Clone()
	for len(tempDg) > 0 {
		terms := tempDg.Terminators()
		if terms.IsEmpty() {
			break
		}
		termSlice := terms.SafeSorted()
		slices.Reverse(termSlice)
		sl = append(sl, termSlice...)
		terms.ForEach(func(t Zid) {
			tempDg.RemoveVertex(t)
		})
	}
	return sl
}

Changes to zettel/id/digraph_test.go.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
}

func TestDigraphOriginators(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   id.EdgeSlice
		orig id.Set
		term id.Set
	}{
		{"empty", nil, nil, nil},
		{"single", zps{{0, 1}}, id.NewSet(0), id.NewSet(1)},
		{"chain", zps{{0, 1}, {1, 2}, {2, 3}}, id.NewSet(0), id.NewSet(3)},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {







|
|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
}

func TestDigraphOriginators(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   id.EdgeSlice
		orig *id.Set
		term *id.Set
	}{
		{"empty", nil, nil, nil},
		{"single", zps{{0, 1}}, id.NewSet(0), id.NewSet(1)},
		{"chain", zps{{0, 1}, {1, 2}, {2, 3}}, id.NewSet(0), id.NewSet(3)},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

func TestDigraphReachableVertices(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name  string
		pairs id.EdgeSlice
		start id.Zid
		exp   id.Set
	}{
		{"nil", nil, 0, nil},
		{"0-2", zps{{1, 2}, {2, 3}}, 1, id.NewSet(2, 3)},
		{"1,2", zps{{1, 2}, {2, 3}}, 2, id.NewSet(3)},
		{"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, id.NewSet(2, 3)},
		{"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 2, id.NewSet(3)},
		{"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 3, nil},







|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

func TestDigraphReachableVertices(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name  string
		pairs id.EdgeSlice
		start id.Zid
		exp   *id.Set
	}{
		{"nil", nil, 0, nil},
		{"0-2", zps{{1, 2}, {2, 3}}, 1, id.NewSet(2, 3)},
		{"1,2", zps{{1, 2}, {2, 3}}, 2, id.NewSet(3)},
		{"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, id.NewSet(2, 3)},
		{"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 2, id.NewSet(3)},
		{"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 3, nil},

Changes to zettel/id/id.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// zettel identifier.
package id

import (
	"strconv"
	"time"

	"zettelstore.de/client.fossil/api"
)

// Zid is the internal identifier of a zettel. Typically, it is a
// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
// A zettelstore implementation should try to set the last two digits to zero,
// e.g. the seconds should be zero,
type Zid uint64







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// zettel identifier.
package id

import (
	"strconv"
	"time"

	"t73f.de/r/zsc/api"
)

// Zid is the internal identifier of a zettel. Typically, it is a
// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
// A zettelstore implementation should try to set the last two digits to zero,
// e.g. the seconds should be zero,
type Zid uint64
42
43
44
45
46
47
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
	ConfigurationZid  = MustParse(api.ZidConfiguration)
	BaseTemplateZid   = MustParse(api.ZidBaseTemplate)
	LoginTemplateZid  = MustParse(api.ZidLoginTemplate)
	ListTemplateZid   = MustParse(api.ZidListTemplate)
	ZettelTemplateZid = MustParse(api.ZidZettelTemplate)
	InfoTemplateZid   = MustParse(api.ZidInfoTemplate)
	FormTemplateZid   = MustParse(api.ZidFormTemplate)
	RenameTemplateZid = MustParse(api.ZidRenameTemplate)
	DeleteTemplateZid = MustParse(api.ZidDeleteTemplate)
	ErrorTemplateZid  = MustParse(api.ZidErrorTemplate)
	StartSxnZid       = MustParse(api.ZidSxnStart)
	BaseSxnZid        = MustParse(api.ZidSxnBase)
	PreludeSxnZid     = MustParse(api.ZidSxnPrelude)
	EmojiZid          = MustParse(api.ZidEmoji)

	TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate)
	DefaultHomeZid    = MustParse(api.ZidDefaultHome)
)

const maxZid = 99999999999999

// ParseUint interprets a string as a possible zettel identifier







<






>







42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
	ConfigurationZid  = MustParse(api.ZidConfiguration)
	BaseTemplateZid   = MustParse(api.ZidBaseTemplate)
	LoginTemplateZid  = MustParse(api.ZidLoginTemplate)
	ListTemplateZid   = MustParse(api.ZidListTemplate)
	ZettelTemplateZid = MustParse(api.ZidZettelTemplate)
	InfoTemplateZid   = MustParse(api.ZidInfoTemplate)
	FormTemplateZid   = MustParse(api.ZidFormTemplate)

	DeleteTemplateZid = MustParse(api.ZidDeleteTemplate)
	ErrorTemplateZid  = MustParse(api.ZidErrorTemplate)
	StartSxnZid       = MustParse(api.ZidSxnStart)
	BaseSxnZid        = MustParse(api.ZidSxnBase)
	PreludeSxnZid     = MustParse(api.ZidSxnPrelude)
	EmojiZid          = MustParse(api.ZidEmoji)
	TOCListsMenuZid   = MustParse(api.ZidTOCListsMenu)
	TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate)
	DefaultHomeZid    = MustParse(api.ZidDefaultHome)
)

const maxZid = 99999999999999

// ParseUint interprets a string as a possible zettel identifier

Changes to zettel/id/set.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
154



155
156


157

158
159
160
161
162





















163
















164






165















166
167
168
169
170
171
172
173



174
175
176
177
178
179
180
181









































































// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package id

import (
	"maps"
	"strings"
)

// Set is a set of zettel identifier
type Set map[Zid]struct{}



// String returns a string representation of the map.
func (s Set) String() string {





	if s == nil {
		return "{}"
	}
	var sb strings.Builder
	sb.WriteByte('{')
	for i, zid := range s.Sorted() {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.Write(zid.Bytes())
	}
	sb.WriteByte('}')
	return sb.String()
}

// NewSet returns a new set of identifier with the given initial values.
func NewSet(zids ...Zid) Set {
	l := len(zids)
	if l < 8 {

		l = 8
	}


	result := make(Set, l)
	result.CopySlice(zids)
	return result
}


// NewSetCap returns a new set of identifier with the given capacity and initial values.
func NewSetCap(c int, zids ...Zid) Set {

	l := len(zids)
	if c < l {
		c = l
	}
	if c < 8 {

		c = 8

	}
	result := make(Set, c)



	result.CopySlice(zids)

	return result
}

// Clone returns a copy of the given set.
func (s Set) Clone() Set {
	if len(s) == 0 {
		return nil
	}
	return maps.Clone(s)
}

// Add adds a Add to the set.
func (s Set) Add(zid Zid) Set {
	if s == nil {
		return NewSet(zid)
	}
	s[zid] = struct{}{}
	return s
}

// Contains return true if the set is non-nil and the set contains the given Zettel identifier.
func (s Set) Contains(zid Zid) bool {
	if s != nil {
		_, found := s[zid]
		return found
	}
	return false
}

// ContainsOrNil return true if the set is nil or if the set contains the given Zettel identifier.
func (s Set) ContainsOrNil(zid Zid) bool {
	if s != nil {
		_, found := s[zid]
		return found
	}
	return true
}

// Copy adds all member from the other set.
func (s Set) Copy(other Set) Set {
	if s == nil {
		if len(other) == 0 {
			return nil
		}
		s = NewSetCap(len(other))
	}
	maps.Copy(s, other)
	return s
}

// CopySlice adds all identifier of the given slice to the set.
func (s Set) CopySlice(sl Slice) Set {
	if s == nil {
		s = NewSetCap(len(sl))
	}

	for _, zid := range sl {
		s[zid] = struct{}{}
	}
	return s
}

// Sorted returns the set as a sorted slice of zettel identifier.
func (s Set) Sorted() Slice {
	if l := len(s); l > 0 {
		result := make(Slice, 0, l)
		for zid := range s {
			result = append(result, zid)
		}
		result.Sort()
		return result
	}
	return nil
}

// IntersectOrSet removes all zettel identifier that are not in the other set.
// Both sets can be modified by this method. One of them is the set returned.
// It contains the intersection of both, if s is not nil.
//
// If s == nil, then the other set is always returned.
func (s Set) IntersectOrSet(other Set) Set {
	if s == nil {
		return other
	}

	if len(s) > len(other) {
		s, other = other, s



	}
	for zid := range s {



		_, otherOk := other[zid]

		if !otherOk {



			delete(s, zid)
		}
	}



	return s
}




// Substract removes all zettel identifier from 's' that are in the set 'other'.
func (s Set) Substract(other Set) {
	if s == nil || other == nil {
		return
	}





















	for zid := range other {
















		delete(s, zid)






	}















}

// Remove the identifier from the set.
func (s Set) Remove(zid Zid) Set {
	if len(s) == 0 {
		return nil
	}
	delete(s, zid)



	if len(s) == 0 {
		return nil
	}
	return s
}

// Equal returns true if the other set is equal to the given set.
func (s Set) Equal(other Set) bool { return maps.Equal(s, other) }
















































































|




|
>
|
>
|
|
>
>
>
>
>
|
|


<
|





<




|
|
|
>
|
<
>
>
|
|
|
|
|
>

|
>
|
<
|
|
|
>
|
>
|
|
>
>
>
|
>
|



|
|


|



|



|




|
<
<
<
|
<
<
<

|
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|

|

>

|




|
|
<
<
|
<
<
<
|

|







|
|


>
|
|
>
>
>
|
|
>
>
>
|
>
|
>
>
>
|
|
|
>
>
>
|
|
>
>
|
>
|
|
|


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



|
|


|
>
>
>
|






|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41
42

43
44
45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98



99



100
101



102















103
104
105
106
107
108
109
110
111
112
113
114
115
116


117



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package id

import (
	"slices"
	"strings"
)

// Set is a set of zettel identifier
type Set struct {
	seq []Zid
}

// String returns a string representation of the set.
func (s *Set) String() string {
	return "{" + s.MetaString() + "}"
}

// MetaString returns a string representation of the set to be stored as metadata.
func (s *Set) MetaString() string {
	if s == nil || len(s.seq) == 0 {
		return ""
	}
	var sb strings.Builder

	for i, zid := range s.seq {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.Write(zid.Bytes())
	}

	return sb.String()
}

// NewSet returns a new set of identifier with the given initial values.
func NewSet(zids ...Zid) *Set {
	switch l := len(zids); l {
	case 0:
		return &Set{seq: nil}
	case 1:

		return &Set{seq: []Zid{zids[0]}}
	default:
		result := Set{seq: make(Slice, 0, l)}
		result.AddSlice(zids)
		return &result
	}
}

// NewSetCap returns a new set of identifier with the given capacity and initial values.
func NewSetCap(c int, zids ...Zid) *Set {
	result := Set{seq: make(Slice, 0, max(c, len(zids)))}
	result.AddSlice(zids)

	return &result
}

// IsEmpty returns true, if the set conains no element.
func (s *Set) IsEmpty() bool {
	return s == nil || len(s.seq) == 0
}

// Length returns the number of elements in this set.
func (s *Set) Length() int {
	if s == nil {
		return 0
	}
	return len(s.seq)
}

// Clone returns a copy of the given set.
func (s *Set) Clone() *Set {
	if s == nil || len(s.seq) == 0 {
		return nil
	}
	return &Set{seq: slices.Clone(s.seq)}
}

// Add adds a Add to the set.
func (s *Set) Add(zid Zid) *Set {
	if s == nil {
		return NewSet(zid)
	}
	s.add(zid)
	return s
}

// Contains return true if the set is non-nil and the set contains the given Zettel identifier.
func (s *Set) Contains(zid Zid) bool { return s != nil && s.contains(zid) }







// ContainsOrNil return true if the set is nil or if the set contains the given Zettel identifier.
func (s *Set) ContainsOrNil(zid Zid) bool { return s == nil || s.contains(zid) }



















// AddSlice adds all identifier of the given slice to the set.
func (s *Set) AddSlice(sl Slice) *Set {
	if s == nil {
		return NewSet(sl...)
	}
	s.seq = slices.Grow(s.seq, len(sl))
	for _, zid := range sl {
		s.add(zid)
	}
	return s
}

// SafeSorted returns the set as a new sorted slice of zettel identifier.
func (s *Set) SafeSorted() Slice {


	if s == nil {



		return nil
	}
	return slices.Clone(s.seq)
}

// IntersectOrSet removes all zettel identifier that are not in the other set.
// Both sets can be modified by this method. One of them is the set returned.
// It contains the intersection of both, if s is not nil.
//
// If s == nil, then the other set is always returned.
func (s *Set) IntersectOrSet(other *Set) *Set {
	if s == nil || other == nil {
		return other
	}
	topos, spos, opos := 0, 0, 0
	for spos < len(s.seq) && opos < len(other.seq) {
		sz, oz := s.seq[spos], other.seq[opos]
		if sz < oz {
			spos++
			continue
		}
		if sz > oz {
			opos++
			continue
		}
		s.seq[topos] = sz
		topos++
		spos++
		opos++
	}
	s.seq = s.seq[:topos]
	return s
}

// IUnion adds the elements of set other to s.
func (s *Set) IUnion(other *Set) *Set {
	if other == nil || len(other.seq) == 0 {
		return s
	}
	// TODO: if other is large enough (and s is not too small) -> optimize by swapping and/or loop through both
	return s.AddSlice(other.seq)
}

// ISubstract removes all zettel identifier from 's' that are in the set 'other'.
func (s *Set) ISubstract(other *Set) {
	if s == nil || len(s.seq) == 0 || other == nil || len(other.seq) == 0 {
		return
	}
	topos, spos, opos := 0, 0, 0
	for spos < len(s.seq) && opos < len(other.seq) {
		sz, oz := s.seq[spos], other.seq[opos]
		if sz < oz {
			s.seq[topos] = sz
			topos++
			spos++
			continue
		}
		if sz == oz {
			spos++
		}
		opos++
	}
	for spos < len(s.seq) {
		s.seq[topos] = s.seq[spos]
		topos++
		spos++
	}
	s.seq = s.seq[:topos]
}

// Diff returns the difference sets between the two sets: the first difference
// set is the set of elements that are in other, but not in s; the second
// difference set is the set of element that are in s but not in other.
//
// in other words: the first result is the set of elements from other that must
// be added to s; the second result is the set of elements that must be removed
// from s, so that s would have the same elemest as other.
func (s *Set) Diff(other *Set) (newS, remS *Set) {
	if s == nil || len(s.seq) == 0 {
		return other.Clone(), nil
	}
	if other == nil || len(other.seq) == 0 {
		return nil, s.Clone()
	}
	seqS, seqO := s.seq, other.seq
	var newRefs, remRefs Slice
	npos, opos := 0, 0
	for npos < len(seqO) && opos < len(seqS) {
		rn, ro := seqO[npos], seqS[opos]
		if rn == ro {
			npos++
			opos++
			continue
		}
		if rn < ro {
			newRefs = append(newRefs, rn)
			npos++
			continue
		}
		remRefs = append(remRefs, ro)
		opos++
	}
	if npos < len(seqO) {
		newRefs = append(newRefs, seqO[npos:]...)
	}
	if opos < len(seqS) {
		remRefs = append(remRefs, seqS[opos:]...)
	}
	return newFromSlice(newRefs), newFromSlice(remRefs)
}

// Remove the identifier from the set.
func (s *Set) Remove(zid Zid) *Set {
	if s == nil || len(s.seq) == 0 {
		return nil
	}
	if pos, found := s.find(zid); found {
		copy(s.seq[pos:], s.seq[pos+1:])
		s.seq = s.seq[:len(s.seq)-1]
	}
	if len(s.seq) == 0 {
		return nil
	}
	return s
}

// Equal returns true if the other set is equal to the given set.
func (s *Set) Equal(other *Set) bool {
	if s == nil {
		return other == nil
	}
	if other == nil {
		return false
	}
	return slices.Equal(s.seq, other.seq)
}

// ForEach calls the given function for each element of the set.
//
// Every element is bigger than the previous one.
func (s *Set) ForEach(fn func(zid Zid)) {
	if s != nil {
		for _, zid := range s.seq {
			fn(zid)
		}
	}
}

// Pop return one arbitrary element of the set.
func (s *Set) Pop() (Zid, bool) {
	if s != nil {
		if l := len(s.seq); l > 0 {
			zid := s.seq[l-1]
			s.seq = s.seq[:l-1]
			return zid, true
		}
	}
	return Invalid, false
}

// Optimize the amount of memory to store the set.
func (s *Set) Optimize() {
	if s != nil {
		s.seq = slices.Clip(s.seq)
	}
}

// ----- unchecked base operations

func newFromSlice(seq Slice) *Set {
	if l := len(seq); l == 0 {
		return nil
	}
	return &Set{seq: seq}
}

func (s *Set) add(zid Zid) {
	if pos, found := s.find(zid); !found {
		s.seq = slices.Insert(s.seq, pos, zid)
	}
}

func (s *Set) contains(zid Zid) bool {
	_, found := s.find(zid)
	return found
}

func (s *Set) find(zid Zid) (int, bool) {
	hi := len(s.seq)
	for lo := 0; lo < hi; {
		m := lo + (hi-lo)/2
		if z := s.seq[m]; z == zid {
			return m, true
		} else if z < zid {
			lo = m + 1
		} else {
			hi = m
		}
	}
	return hi, false
}

Changes to zettel/id/set_test.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113





























































































114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

import (
	"testing"

	"zettelstore.de/z/zettel/id"
)

func TestSetContains(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s   id.Set
		zid id.Zid
		exp bool
	}{
		{nil, id.Invalid, true},
		{nil, 14, true},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(), 1, false},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(1), 1, true},
	}
	for i, tc := range testcases {
		got := tc.s.ContainsOrNil(tc.zid)
		if got != tc.exp {
			t.Errorf("%d: %v.Contains(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got)
		}
	}
}

func TestSetAdd(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1, 2}},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.Sorted()
		sl2 := tc.s2.Sorted()
		got := tc.s1.Copy(tc.s2).Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetSorted(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		set id.Set
		exp id.Slice
	}{
		{nil, nil},
		{id.NewSet(), nil},
		{id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}},
	}
	for i, tc := range testcases {
		got := tc.set.Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got)
		}
	}
}

func TestSetIntersectOrSet(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), id.NewSet(), nil},
		{id.NewSet(), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(2), nil},
		{id.NewSet(2), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.Sorted()
		sl2 := tc.s2.Sorted()
		got := tc.s1.IntersectOrSet(tc.s2).Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}






























































































func TestSetRemove(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1}},
		{id.NewSet(1), id.NewSet(1), id.Slice{}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.Sorted()
		sl2 := tc.s2.Sorted()
		newS1 := id.NewSet(sl1...)
		newS1.Substract(tc.s2)
		got := newS1.Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

//	func BenchmarkSet(b *testing.B) {
//		s := id.Set{}
//		for range b.N {
//			s[id.Zid(i)] = true
//		}
//	}
func BenchmarkSet(b *testing.B) {
	s := id.Set{}
	for i := range b.N {
		s[id.Zid(i)] = struct{}{}
	}
}







|


|













|







|












|
|
|






|


|







|

|







|















|
|
|





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|











|
|

|
|






<
<
<
<
<
<

|

|


15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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

import (
	"testing"

	"zettelstore.de/z/zettel/id"
)

func TestSetContainsOrNil(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s   *id.Set
		zid id.Zid
		exp bool
	}{
		{nil, id.Invalid, true},
		{nil, 14, true},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(), 1, false},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(1), 1, true},
	}
	for i, tc := range testcases {
		got := tc.s.ContainsOrNil(tc.zid)
		if got != tc.exp {
			t.Errorf("%d: %v.ContainsOrNil(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got)
		}
	}
}

func TestSetAdd(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1, 2}},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := tc.s1.IUnion(tc.s2).SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetSafeSorted(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		set *id.Set
		exp id.Slice
	}{
		{nil, nil},
		{id.NewSet(), nil},
		{id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}},
	}
	for i, tc := range testcases {
		got := tc.set.SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.SafeSorted() should be %v, but got %v", i, tc.set, tc.exp, got)
		}
	}
}

func TestSetIntersectOrSet(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), id.NewSet(), nil},
		{id.NewSet(), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(2), nil},
		{id.NewSet(2), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := tc.s1.IntersectOrSet(tc.s2).SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetIUnion(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    *id.Set
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.NewSet(1)},
		{nil, id.NewSet(1), id.NewSet(1)},
		{id.NewSet(1), id.NewSet(), id.NewSet(1)},
		{id.NewSet(), id.NewSet(1), id.NewSet(1)},
		{id.NewSet(1), id.NewSet(2), id.NewSet(1, 2)},
		{id.NewSet(2), id.NewSet(1), id.NewSet(2, 1)},
		{id.NewSet(1), id.NewSet(1), id.NewSet(1)},
		{id.NewSet(1, 2, 3), id.NewSet(2, 3, 4), id.NewSet(1, 2, 3, 4)},
	}
	for i, tc := range testcases {
		s1 := tc.s1.Clone()
		sl1 := s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := s1.IUnion(tc.s2)
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.IUnion(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetISubtract(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.Slice{1}},
		{nil, id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(2), id.Slice{1}},
		{id.NewSet(2), id.NewSet(1), id.Slice{2}},
		{id.NewSet(1), id.NewSet(1), nil},
		{id.NewSet(1, 2, 3), id.NewSet(1), id.Slice{2, 3}},
		{id.NewSet(1, 2, 3), id.NewSet(2), id.Slice{1, 3}},
		{id.NewSet(1, 2, 3), id.NewSet(3), id.Slice{1, 2}},
		{id.NewSet(1, 2, 3), id.NewSet(1, 2), id.Slice{3}},
		{id.NewSet(1, 2, 3), id.NewSet(1, 3), id.Slice{2}},
		{id.NewSet(1, 2, 3), id.NewSet(2, 3), id.Slice{1}},
	}
	for i, tc := range testcases {
		s1 := tc.s1.Clone()
		sl1 := s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		s1.ISubstract(tc.s2)
		got := s1.SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.ISubstract(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetDiff(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in1, in2   *id.Set
		exp1, exp2 *id.Set
	}{
		{nil, nil, nil, nil},
		{id.NewSet(1), nil, nil, id.NewSet(1)},
		{nil, id.NewSet(1), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(1), nil, nil},
		{id.NewSet(1, 2), id.NewSet(1), nil, id.NewSet(2)},
		{id.NewSet(1), id.NewSet(1, 2), id.NewSet(2), nil},
		{id.NewSet(1, 2), id.NewSet(1, 3), id.NewSet(3), id.NewSet(2)},
		{id.NewSet(1, 2, 3), id.NewSet(2, 3, 4), id.NewSet(4), id.NewSet(1)},
		{id.NewSet(2, 3, 4), id.NewSet(1, 2, 3), id.NewSet(1), id.NewSet(4)},
	}
	for i, tc := range testcases {
		gotN, gotO := tc.in1.Diff(tc.in2)
		if !tc.exp1.Equal(gotN) {
			t.Errorf("%d: expected %v, but got: %v", i, tc.exp1, gotN)
		}
		if !tc.exp2.Equal(gotO) {
			t.Errorf("%d: expected %v, but got: %v", i, tc.exp2, gotO)
		}
	}
}

func TestSetRemove(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1}},
		{id.NewSet(1), id.NewSet(1), id.Slice{}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		newS1 := id.NewSet(sl1...)
		newS1.ISubstract(tc.s2)
		got := newS1.SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}







func BenchmarkSet(b *testing.B) {
	s := id.NewSetCap(b.N)
	for i := range b.N {
		s.Add(id.Zid(i))
	}
}

Changes to zettel/id/slice.go.

27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
// Clone a zettel identifier slice
func (zs Slice) Clone() Slice { return slices.Clone(zs) }

// Equal reports whether zs and other are the same length and contain the samle zettel
// identifier. A nil argument is equivalent to an empty slice.
func (zs Slice) Equal(other Slice) bool { return slices.Equal(zs, other) }


func (zs Slice) String() string {
	if len(zs) == 0 {
		return ""
	}
	var sb strings.Builder
	for i, zid := range zs {
		if i > 0 {
			sb.WriteByte(' ')







>
|







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Clone a zettel identifier slice
func (zs Slice) Clone() Slice { return slices.Clone(zs) }

// Equal reports whether zs and other are the same length and contain the samle zettel
// identifier. A nil argument is equivalent to an empty slice.
func (zs Slice) Equal(other Slice) bool { return slices.Equal(zs, other) }

// MetaString returns the slice as a string to be store in metadata.
func (zs Slice) MetaString() string {
	if len(zs) == 0 {
		return ""
	}
	var sb strings.Builder
	for i, zid := range zs {
		if i > 0 {
			sb.WriteByte(' ')

Changes to zettel/id/slice_test.go.

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
		got = tc.s2.Equal(tc.s1)
		if got != tc.exp {
			t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got)
		}
	}
}

func TestSliceString(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  id.Slice
		exp string
	}{
		{nil, ""},
		{id.Slice{}, ""},
		{id.Slice{1}, "00000000000001"},
		{id.Slice{1, 2}, "00000000000001 00000000000002"},
	}
	for i, tc := range testcases {
		got := tc.in.String()
		if got != tc.exp {
			t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got)
		}
	}
}







|











|





65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
		got = tc.s2.Equal(tc.s1)
		if got != tc.exp {
			t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got)
		}
	}
}

func TestSliceMetaString(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  id.Slice
		exp string
	}{
		{nil, ""},
		{id.Slice{}, ""},
		{id.Slice{1}, "00000000000001"},
		{id.Slice{1, 2}, "00000000000001 00000000000002"},
	}
	for i, tc := range testcases {
		got := tc.in.MetaString()
		if got != tc.exp {
			t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got)
		}
	}
}

Changes to zettel/meta/collection.go.

9
10
11
12
13
14
15
16



17
18
19
20
21
22
23
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

import "sort"




// Arrangement stores metadata within its categories.
// Typecally a category might be a tag name, a role name, a syntax value.
type Arrangement map[string][]*Meta

// CreateArrangement by inspecting a given key and use the found
// value as a category.







|
>
>
>







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

import (
	"slices"
	"strings"
)

// Arrangement stores metadata within its categories.
// Typecally a category might be a tag name, a role name, a syntax value.
type Arrangement map[string][]*Meta

// CreateArrangement by inspecting a given key and use the found
// value as a category.
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Every name must occur only once.
type CountedCategories []CountedCategory

// SortByName sorts the list by the name attribute.
// Since each name must occur only once, two CountedCategories cannot have
// the same name.
func (ccs CountedCategories) SortByName() {
	sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name })
}

// SortByCount sorts the list by the count attribute, descending.
// If two counts are equal, elements are sorted by name.
func (ccs CountedCategories) SortByCount() {
	sort.Slice(ccs, func(i, j int) bool {
		iCount, jCount := ccs[i].Count, ccs[j].Count
		if iCount > jCount {
			return true
		}
		if iCount == jCount {
			return ccs[i].Name < ccs[j].Name
		}
		return false
	})
}

// Categories returns just the category names.
func (ccs CountedCategories) Categories() []string {
	result := make([]string, len(ccs))
	for i, cc := range ccs {
		result[i] = cc.Name
	}
	return result
}







|





|
|

|


|

|











84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Every name must occur only once.
type CountedCategories []CountedCategory

// SortByName sorts the list by the name attribute.
// Since each name must occur only once, two CountedCategories cannot have
// the same name.
func (ccs CountedCategories) SortByName() {
	slices.SortFunc(ccs, func(i, j CountedCategory) int { return strings.Compare(i.Name, j.Name) })
}

// SortByCount sorts the list by the count attribute, descending.
// If two counts are equal, elements are sorted by name.
func (ccs CountedCategories) SortByCount() {
	slices.SortFunc(ccs, func(i, j CountedCategory) int {
		iCount, jCount := i.Count, j.Count
		if iCount > jCount {
			return -1
		}
		if iCount == jCount {
			return strings.Compare(i.Name, j.Name)
		}
		return 1
	})
}

// Categories returns just the category names.
func (ccs CountedCategories) Categories() []string {
	result := make([]string, len(ccs))
	for i, cc := range ccs {
		result[i] = cc.Name
	}
	return result
}

Changes to zettel/meta/meta.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//-----------------------------------------------------------------------------

// Package meta provides the zettel specific type 'meta'.
package meta

import (
	"regexp"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type keyUsage int

const (







|




|
|
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//-----------------------------------------------------------------------------

// Package meta provides the zettel specific type 'meta'.
package meta

import (
	"regexp"
	"slices"
	"strings"
	"unicode"
	"unicode/utf8"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type keyUsage int

const (
113
114
115
116
117
118
119




120
121
122
123
124
125
126
127
128
129

130
131
132
133
134
135
136
137
138
139
140

141
142
143
144
145
146
147
148
149

150
151
152
153
154
155
156
	result := make([]*DescriptionKey, 0, len(keys))
	for _, n := range keys {
		result = append(result, registeredKeys[n])
	}
	return result
}





// Supported keys.
func init() {
	registerKey(api.KeyID, TypeID, usageComputed, "")
	registerKey(api.KeyTitle, TypeEmpty, usageUser, "")
	registerKey(api.KeyRole, TypeWord, usageUser, "")
	registerKey(api.KeyTags, TypeTagSet, usageUser, "")
	registerKey(api.KeySyntax, TypeWord, usageUser, "")

	// Properties that are inverse keys
	registerKey(api.KeyFolge, TypeIDSet, usageProperty, "")

	registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "")
	registerKey(api.KeySubordinates, TypeIDSet, usageProperty, "")

	// Non-inverse keys
	registerKey(api.KeyAuthor, TypeString, usageUser, "")
	registerKey(api.KeyBack, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBackward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "")
	registerKey(api.KeyCopyright, TypeString, usageUser, "")
	registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyCredential, TypeCredential, usageUser, "")

	registerKey(api.KeyDead, TypeIDSet, usageProperty, "")
	registerKey(api.KeyExpire, TypeTimestamp, usageUser, "")
	registerKey(api.KeyFolgeRole, TypeWord, usageUser, "")
	registerKey(api.KeyForward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyLang, TypeWord, usageUser, "")
	registerKey(api.KeyLicense, TypeEmpty, usageUser, "")
	registerKey(api.KeyModified, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge)
	registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors)

	registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "")
	registerKey(api.KeyQuery, TypeEmpty, usageUser, "")
	registerKey(api.KeyReadOnly, TypeWord, usageUser, "")
	registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeySuperior, TypeIDSet, usageUser, api.KeySubordinates)
	registerKey(api.KeyURL, TypeURL, usageUser, "")
	registerKey(api.KeyUselessFiles, TypeString, usageProperty, "")







>
>
>
>










>











>









>







113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
	result := make([]*DescriptionKey, 0, len(keys))
	for _, n := range keys {
		result = append(result, registeredKeys[n])
	}
	return result
}

// KeyCreatedMissing is temporary until migration to B36 has ended.
// It is not an "official" key to be designed to last long.
const KeyCreatedMissing = "created-missing"

// Supported keys.
func init() {
	registerKey(api.KeyID, TypeID, usageComputed, "")
	registerKey(api.KeyTitle, TypeEmpty, usageUser, "")
	registerKey(api.KeyRole, TypeWord, usageUser, "")
	registerKey(api.KeyTags, TypeTagSet, usageUser, "")
	registerKey(api.KeySyntax, TypeWord, usageUser, "")

	// Properties that are inverse keys
	registerKey(api.KeyFolge, TypeIDSet, usageProperty, "")
	registerKey(api.KeySequel, TypeIDSet, usageProperty, "")
	registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "")
	registerKey(api.KeySubordinates, TypeIDSet, usageProperty, "")

	// Non-inverse keys
	registerKey(api.KeyAuthor, TypeString, usageUser, "")
	registerKey(api.KeyBack, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBackward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "")
	registerKey(api.KeyCopyright, TypeString, usageUser, "")
	registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyCredential, TypeCredential, usageUser, "")
	registerKey(KeyCreatedMissing, TypeWord, usageProperty, "")
	registerKey(api.KeyDead, TypeIDSet, usageProperty, "")
	registerKey(api.KeyExpire, TypeTimestamp, usageUser, "")
	registerKey(api.KeyFolgeRole, TypeWord, usageUser, "")
	registerKey(api.KeyForward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyLang, TypeWord, usageUser, "")
	registerKey(api.KeyLicense, TypeEmpty, usageUser, "")
	registerKey(api.KeyModified, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge)
	registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors)
	registerKey(api.KeyPrequel, TypeIDSet, usageUser, api.KeySequel)
	registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "")
	registerKey(api.KeyQuery, TypeEmpty, usageUser, "")
	registerKey(api.KeyReadOnly, TypeWord, usageUser, "")
	registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeySuperior, TypeIDSet, usageUser, api.KeySubordinates)
	registerKey(api.KeyURL, TypeURL, usageUser, "")
	registerKey(api.KeyUselessFiles, TypeString, usageProperty, "")
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string {
	keys := make([]string, 0, len(m.pairs))
	for k := range m.pairs {
		if !firstKeySet.Has(k) && addKeyPred(k) {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)
	return keys
}

// Delete removes a key from the data.
func (m *Meta) Delete(key string) {
	if key != api.KeyID {
		delete(m.pairs, key)







|







342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string {
	keys := make([]string, 0, len(m.pairs))
	for k := range m.pairs {
		if !firstKeySet.Has(k) && addKeyPred(k) {
			keys = append(keys, k)
		}
	}
	slices.Sort(keys)
	return keys
}

// Delete removes a key from the data.
func (m *Meta) Delete(key string) {
	if key != api.KeyID {
		delete(m.pairs, key)

Changes to zettel/meta/meta_test.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package meta

import (
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
)

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	t.Parallel()







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package meta

import (
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
)

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	t.Parallel()

Changes to zettel/meta/parse.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
//-----------------------------------------------------------------------------

package meta

import (
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
	if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
		skipToEOL(inp)
		inp.EatEOL()
	}
	meta := New(zid)
	for {
		skipSpace(inp)
		switch inp.Ch {
		case '\r':
			if inp.Peek() == '\n' {
				inp.Next()
			}
			fallthrough
		case '\n':







|
|
|












|







12
13
14
15
16
17
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 meta

import (
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
	if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
		skipToEOL(inp)
		inp.EatEOL()
	}
	meta := New(zid)
	for {
		inp.SkipSpace()
		switch inp.Ch {
		case '\r':
			if inp.Peek() == '\n' {
				inp.Next()
			}
			fallthrough
		case '\n':
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

func parseHeader(m *Meta, inp *input.Input) {
	pos := inp.Pos
	for isHeader(inp.Ch) {
		inp.Next()
	}
	key := inp.Src[pos:inp.Pos]
	skipSpace(inp)
	if inp.Ch == ':' {
		inp.Next()
	}
	var val []byte
	for {
		skipSpace(inp)
		pos = inp.Pos
		skipToEOL(inp)
		val = append(val, inp.Src[pos:inp.Pos]...)
		inp.EatEOL()
		if !input.IsSpace(inp.Ch) {
			break
		}
		val = append(val, ' ')
	}
	addToMeta(m, string(key), string(val))
}

func skipSpace(inp *input.Input) {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	}
}

func skipToEOL(inp *input.Input) {
	for {
		switch inp.Ch {
		case '\n', '\r', input.EOS:
			return
		}
		inp.Next()







|





|




|







<
<
<
<
<
<







60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85






86
87
88
89
90
91
92

func parseHeader(m *Meta, inp *input.Input) {
	pos := inp.Pos
	for isHeader(inp.Ch) {
		inp.Next()
	}
	key := inp.Src[pos:inp.Pos]
	inp.SkipSpace()
	if inp.Ch == ':' {
		inp.Next()
	}
	var val []byte
	for {
		inp.SkipSpace()
		pos = inp.Pos
		skipToEOL(inp)
		val = append(val, inp.Src[pos:inp.Pos]...)
		inp.EatEOL()
		if !inp.IsSpace() {
			break
		}
		val = append(val, ' ')
	}
	addToMeta(m, string(key), string(val))
}







func skipToEOL(inp *input.Input) {
	for {
		switch inp.Ch {
		case '\n', '\r', input.EOS:
			return
		}
		inp.Next()

Changes to zettel/meta/parse_test.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

package meta_test

import (
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/input"
	"zettelstore.de/z/zettel/meta"
)

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput([]byte(src)))
}








|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

package meta_test

import (
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/zettel/meta"
)

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput([]byte(src)))
}

Changes to zettel/meta/type.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"strconv"
	"strings"
	"sync"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import (
	"strconv"
	"strings"
	"sync"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
89
90
91
92
93
94
95
96
97
98
99
100

101
102
103
104


105

106
107
108
109
110
111
112
// Type returns a type hint for the given key. If no type hint is specified,
// TypeEmpty is returned.
func Type(key string) *DescriptionType {
	if k, ok := registeredKeys[key]; ok {
		return k.Type
	}
	mxTypedKey.RLock()
	k, ok := cachedTypedKeys[key]
	mxTypedKey.RUnlock()
	if ok {
		return k
	}

	for suffix, t := range suffixTypes {
		if strings.HasSuffix(key, suffix) {
			mxTypedKey.Lock()
			defer mxTypedKey.Unlock()


			cachedTypedKeys[key] = t

			return t
		}
	}
	return TypeEmpty
}

// SetList stores the given string list value under the given key.







|

|


>




>
>
|
>







89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Type returns a type hint for the given key. If no type hint is specified,
// TypeEmpty is returned.
func Type(key string) *DescriptionType {
	if k, ok := registeredKeys[key]; ok {
		return k.Type
	}
	mxTypedKey.RLock()
	k, found := cachedTypedKeys[key]
	mxTypedKey.RUnlock()
	if found {
		return k
	}

	for suffix, t := range suffixTypes {
		if strings.HasSuffix(key, suffix) {
			mxTypedKey.Lock()
			defer mxTypedKey.Unlock()
			// Double check to avoid races
			if _, found = cachedTypedKeys[key]; !found {
				cachedTypedKeys[key] = t
			}
			return t
		}
	}
	return TypeEmpty
}

// SetList stores the given string list value under the given key.

Changes to zettel/meta/values.go.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package meta

import (
	"fmt"

	"zettelstore.de/client.fossil/api"
)

// Supported syntax values.
const (
	SyntaxCSS      = api.ValueSyntaxCSS
	SyntaxDraw     = api.ValueSyntaxDraw
	SyntaxGif      = api.ValueSyntaxGif







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//-----------------------------------------------------------------------------

package meta

import (
	"fmt"

	"t73f.de/r/zsc/api"
)

// Supported syntax values.
const (
	SyntaxCSS      = api.ValueSyntaxCSS
	SyntaxDraw     = api.ValueSyntaxDraw
	SyntaxGif      = api.ValueSyntaxGif

Changes to zettel/meta/write_test.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package meta_test

import (
	"strings"
	"testing"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const testID = id.Zid(98765432101234)

func newMeta(title string, tags []string, syntax string) *meta.Meta {







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package meta_test

import (
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const testID = id.Zid(98765432101234)

func newMeta(title string, tags []string, syntax string) *meta.Meta {