Zettelstore

Check-in Differences
Login

Check-in Differences

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

Difference From trunk To v0.18.0

2024-11-28
13:29
Manual: remove info about previous new-style zids; fix some typos ... (Leaf check-in: d5f6fa92b2 user: stern tags: trunk)
2024-11-27
17:18
Remove support of mapping zids to previous new-style zids. ... (check-in: ca58093de8 user: stern tags: trunk)
2024-07-11
15:34
Increase version to 0.19.0-dev to begin next development cycle ... (check-in: 1d1cd5e637 user: stern tags: trunk)
14:43
Version 0.18.0 ... (check-in: b94ede10d4 user: stern tags: trunk, release, v0.18.0)
14:14
Add KEYS aggregate action to API manual ... (check-in: a6d7c963a1 user: stern tags: trunk)

Changes to VERSION.

1


1
-
+
0.19.0-dev
0.18.0

Changes to auth/auth.go.

90
91
92
93
94
95
96



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







+
+
+







	CanCreate(user, newMeta *meta.Meta) bool

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

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

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

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

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

Changes to auth/policy/anon.go.

31
32
33
34
35
36
37




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







+
+
+
+







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

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

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

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

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

Changes to auth/policy/box.go.

117
118
119
120
121
122
123
















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







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







		return err
	}
	if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) {
		return pp.box.UpdateZettel(ctx, zettel)
	}
	return box.NewErrNotAllowed("Write", user, zid)
}

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

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

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

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

Changes to auth/policy/default.go.

24
25
26
27
28
29
30

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







+







}

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

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

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

Changes to auth/policy/owner.go.

110
111
112
113
114
115
116










117
118
119
120
121
122
123
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133







+
+
+
+
+
+
+
+
+
+







	}
	switch userRole := o.manager.GetUserRole(user); userRole {
	case meta.UserRoleReader, meta.UserRoleCreator:
		return false
	}
	return o.userCanCreate(user, newMeta)
}

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

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

Changes to auth/policy/policy.go.

55
56
57
58
59
60
61




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







+
+
+
+







	return m != nil && p.post.CanRead(user, m)
}

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

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

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

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

Changes to auth/policy/policy_test.go.

55
56
57
58
59
60
61

62
63
64
65
66
67
68
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 {
389
390
391
392
393
394
395
























































































396
397
398
399
400
401
402
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







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







		{writer, roTrue, roTrue, false},
		{owner, roTrue, roTrue, false},
		{owner2, roTrue, roTrue, false},
	}
	for _, tc := range testCases {
		t.Run("Write", func(tt *testing.T) {
			got := pol.CanWrite(tc.user, tc.old, tc.new)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

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

Changes to auth/policy/readonly.go.

16
17
18
19
20
21
22

23
24
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 }

Changes to box/box.go.

32
33
34
35
36
37
38






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







+
+
+
+
+
+







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







-
+
-












-
-
-







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
	OnZettel              // Something with a 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)
}

Changes to box/compbox/compbox.go.

28
29
30
31
32
33
34
35
36


37
38
39
40
41
42
43

44
45
46
47
48
49
50
28
29
30
31
32
33
34


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







-
-
+
+







+







	"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
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return getCompBox(cdata.Number, cdata.Enricher, cdata.Mapper), nil
		})
}

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

var myConfig *meta.Meta
var myZettel = map[id.Zid]struct {
	meta    func(id.Zid) *meta.Meta
	content func(context.Context, *compBox) []byte
}{
60
61
62
63
64
65
66


67
68
69
70

71
72
73
74
75

76
77
78
79
80
81
82
61
62
63
64
65
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80
81
82
83
84
85
86







+
+



-
+





+







	// 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},
	id.MustParse(api.ZidWarnings):             {genWarningsM, genWarningsC},
	id.MustParse(api.ZidMapping):              {genMappingM, genMappingC},
}

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

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

func (*compBox) Location() string { return "" }
133
134
135
136
137
138
139















140
141
142
143
144
145
146
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165







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







				cb.enricher.Enrich(ctx, m, cb.number)
				handle(m)
			}
		}
	}
	return nil
}

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

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

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

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

Added box/compbox/mapping.go.


































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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) 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"

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

// Zettelstore Identifier Mapping.
//
// In the first stage of migration process, it is a computed zettel showing a
// hypothetical mapping. In later stages, it will be stored as a normal zettel
// that is updated when a new zettel is created or an old zettel is deleted.

func genMappingM(zid id.Zid) *meta.Meta {
	return getTitledMeta(zid, "Zettelstore Identifier Mapping")
}

func genMappingC(ctx context.Context, cb *compBox) []byte {
	var buf bytes.Buffer
	toNew, err := cb.mapper.OldToNewMapping(ctx)
	if err != nil {
		buf.WriteString("**Error while fetching: ")
		buf.WriteString(err.Error())
		buf.WriteString("**\n")
		return buf.Bytes()
	}
	oldZids := id.NewSetCap(len(toNew))
	for zidO := range toNew {
		oldZids.Add(zidO)
	}
	first := true
	oldZids.ForEach(func(zidO id.Zid) {
		if first {
			buf.WriteString("**Note**: this mapping is preliminary.\n")
			buf.WriteString("It only shows you how it could look if the migration is done.\n")
			buf.WriteString("Use this page to update your zettel if something strange is shown.\n")
			buf.WriteString("```\n")
			first = false
		}
		buf.WriteString(zidO.String())
		buf.WriteByte(' ')
		buf.WriteString(toNew[zidO].String())
		buf.WriteByte('\n')
	})
	if !first {
		buf.WriteString("```")
	}
	return buf.Bytes()
}

Changes to box/compbox/memory.go.

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


45
46
47
48
49
50
51







-
-







	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 → %d\n",
				i, bysize.Size, bysize.Mallocs, bysize.Frees, bysize.Mallocs-bysize.Frees)
		}
	}

Added box/compbox/warnings.go.





















































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

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

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

func genWarningsC(ctx context.Context, cb *compBox) []byte {
	var buf bytes.Buffer
	buf.WriteString("* [[Zettel without stored creation date|query:created-missing:true]]\n")
	buf.WriteString("* [[Zettel with strange creation date|query:created<19700101000000]]\n")

	ws, err := cb.mapper.Warnings(ctx)
	if err != nil {
		buf.WriteString("**Error while fetching: ")
		buf.WriteString(err.Error())
		buf.WriteString("**\n")
		return buf.Bytes()
	}
	first := true
	ws.ForEach(func(zid id.Zid) {
		if first {
			first = false
			buf.WriteString("=== Mapper Warnings\n")
		}
		buf.WriteString("* [[")
		buf.WriteString(zid.String())
		buf.WriteString("]]\n")
	})

	return buf.Bytes()
}

Changes to box/constbox/base.css.

12
13
14
15
16
17
18

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







+







 *-----------------------------------------------------------------------------
 */

*,*::before,*::after {
    box-sizing: border-box;
  }
  html {
    font-size: 1rem;
    font-family: serif;
    scroll-behavior: smooth;
    height: 100%;
  }
  body {
    margin: 0;
    min-height: 100vh;
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99









100
101

102
103

104
105
106
107
108
109





110
111


112
113
114
115
116
117
118
119
120







121
122
123
124
125
126
127
85
86
87
88
89
90
91









92
93
94
95
96
97
98
99
100
101

102
103

104
105
106




107
108
109
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; 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 }
  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: .5em 0 0 0 }
  dt { margin: .5rem 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5em 0 0 2em }
  dd { margin: .5rem 0 0 2rem }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: .5em solid lightgray;
    padding-left: 1em;
    margin-left: 1em;
    margin-right: 2em;
    border-left: 0.5rem solid lightgray;
    padding-left: 1rem;
    margin-left: 1rem;
    margin-right: 2rem;
    font-style: italic;
  }
  blockquote p { margin-bottom: .5em }
  blockquote p { margin-bottom: .5rem }
  blockquote cite { font-style: normal }
  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%) }
  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;
149
150
151
152
153
154
155
156

157
158
159
160

161
162
163
164
165
166
167
168
169

170
171
172

173
174
175

176
177
178
179

180
181
182
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
155
156
157
158
159
160
161

162
163
164
165

166
167
168
169
170
171
172
173
174

175
176
177

178
179
180

181
182
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[rel~="external"]::after { content: "➚"; display: inline-block }
  a.external::after { content: "➚"; display: inline-block }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5em;
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1em .2em;
    padding: .1rem .2rem;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25em;
    border-radius: .25rem;
  }
  pre {
    padding: .5em .7em;
    padding: .5rem .7rem;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5em;
    border-radius: .5rem;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5em .7em;
    padding: .5rem .7rem;
    max-width: 100%;
    border-radius: .5em;
    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: .25em;
    padding: .1rem .2em;
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 95%;
  }
  .zs-info {
    background-color: lightblue;
    padding: .5em 1em;
    padding: .5rem 1rem;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5em 1em;
    padding: .5rem 1rem;
  }
  .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 }
  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: .2em }
  .zs-deprecated { border-style: dashed; padding: .2rem }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1em;
    margin-bottom:1rem;
  }
  .zs-meta a { color:#444 }
  h1+.zs-meta { margin-top:-1em }
  nav > details { margin-top:1em }
  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:2em;
    padding-left:2rem;
    background-color: #eee;
  }
  footer { padding: 0 1em }
  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;
    }
  }

Changes to box/constbox/constbox.go.

29
30
31
32
33
34
35
36

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

36
37
38
39
40
41
42
43







-
+







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

func init() {
	manager.Register(
		" const",
		func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		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
92
93
94
95
96
97
98















99
100
101
102
103
104
105
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120







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







			m := meta.NewWithData(zid, zettel.header)
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

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

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

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

func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {
	if _, ok := cb.zettel[zid]; ok {
		err = box.ErrReadOnly
	} else {
182
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
197
198
199
200
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:   "20241127170400",
			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:   "20241127170500",
			api.KeyModified:   "20240618170000",
			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:   "20241127170530",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
253
254
255
256
257
258
259
260

261
262
263
264
265
266
267
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:   "20241118173500",
			api.KeyModified:   "20240618170100",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnPrelude),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.PreludeSxnZid: {
		constHeader{
276
277
278
279
280
281
282
283

284
285
286
287
288
289
290
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:   "20240827143500",
			api.KeyModified:   "20231129112800",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
440
441
442
443
444
445
446



447
448
449
450
451
452
453
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481







+
+
+








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

Changes to box/constbox/info.sxn.

16
17
18
19
20
21
22

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







+







    (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-local-link local-links)))) 

Added box/constbox/rename.sxn.











































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

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

Changes to box/constbox/wuicode.sxn.

34
35
36
37
38
39
40
41

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

41
42
43
44
45
46
47
48







-
+







;; 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)))
    `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e)))

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

;; wui-datalist returns a HTML datalist with the given HTML identifier and a
;; list of values.
(defun wui-datalist (id lst)
88
89
90
91
92
93
94
95
96


97
98
99
100
101
102
103
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 ((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)

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

Changes to box/dirbox/dirbox.go.

199
200
201
202
203
204
205
206
207
208
209




210
211
212
213
214
215
216
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) 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
240
241
242
243
244
245
246
247

248
249
250
251
252
253
254
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.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() {
312
313
314
315
316
317
318
319

320
321
322
323
324
325
326
327














































328
329
330
331
332
333
334
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







-
+








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







		// 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.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()
345
346
347
348
349
350
351
352

353
354
355
356
357
358
359
360
361
362
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, box.OnDelete)
		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")
}

Changes to box/filebox/zipbox.go.

31
32
33
34
35
36
37
38

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

38
39
40
41
42
43
44
45







-
+







)

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

func (zb *zipBox) Location() string {
	if strings.HasPrefix(zb.name, "/") {
		return "file://" + zb.name
	}
167
168
169
170
171
172
173


















174
175
176
177
178
179
180
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198







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







			continue
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}

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

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

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

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

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
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 {
func GetQueryInt(u *url.URL, key string, min, def, max int) int {
	sVal := u.Query().Get(key)
	if sVal == "" {
		return defVal
		return def
	}
	iVal, err := strconv.Atoi(sVal)
	if err != nil {
		return defVal
		return def
	}
	if iVal < minVal {
		return minVal
	if iVal < min {
		return min
	}
	if iVal > maxVal {
		return maxVal
	if iVal > max {
		return max
	}
	return iVal
}

Changes to box/manager/box.go.

52
53
54
55
56
57
58
59

60
61
62
63
64
65
66
67
68


69
70

71
72

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
52
53
54
55
56
57
58

59
60
61
62
63
64
65
66


67
68
69

70
71

72
73
74
75
76
77
78
79
80
81
82
83
84



85
86
87
88
89
90
91







-
+







-
-
+
+

-
+

-
+












-
-
-







	if 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) {
func (mgr *Manager) CreateZettel(ctx context.Context, zettel 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)
		zidO, err := box.CreateZettel(ctx, ztl)
		zettel.Meta = mgr.cleanMetaProperties(zettel.Meta)
		zid, err := box.CreateZettel(ctx, zettel)
		if err == nil {
			mgr.idxUpdateZettel(ctx, ztl)
			mgr.idxUpdateZettel(ctx, zettel)
		}
		return zidO, err
		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
138
139
140
141
142
143
144
145
146
147
148
149
150
151

152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
135
136
137
138
139
140
141







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

157
158
159
160
161
162
163
164

165
166
167
168
169
170
171







-
-
-
-
-
-
-
+














-








-







		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

// FetchZidsO returns the set of all old-style zettel identifer managed by the box.
func (mgr *Manager) FetchZidsO(ctx context.Context) (*id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZidsO")
	return mgr.fetchZids(ctx)
}

func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool {
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
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
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







-
-
-










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

















-
-
+
+






-
+

-
-
+
+






-
+













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

// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(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.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 err := mgr.checkContinue(ctx); err != nil {
		return err
	}
	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 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, zidO id.Zid) error {
	mgr.mgrLog.Debug().Zid(zidO).Msg("DeleteZettel")
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, zidO)
		err := p.DeleteZettel(ctx, zid)
		if err == nil {
			mgr.idxDeleteZettel(ctx, zidO)
			return err
			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: zidO}
	return box.ErrZettelNotFound{Zid: zid}
}

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

Changes to box/manager/enrich.go.

21
22
23
24
25
26
27

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







+







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

Changes to box/manager/indexer.go.

153
154
155
156
157
158
159
160
161

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
153
154
155
156
157
158
159


160

161
162
163
164
165
166
167
168
169











170
171
172
173
174
175
176







-
-
+
-









-
-
-
-
-
-
-
-
-
-
-







	}
	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)
	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 {
	zid := m.Zid
	if zid >= id.DefaultHomeZid {
		return true
	}
	if zid >= id.Zid(100000000) && zid < id.Zid(9999999997) {
		return true
	}
	return false
}

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







-
+














-
+









+
+
+
+
+











	} 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) {
		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) {
	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) {
	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
34
35
36
37
38
39
40

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







-
+
+
+
+
+
+
+
+
+







)

// 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
	Notify   chan<- box.UpdateInfo
	Mapper   Mapper
}

// Mapper allows to inspect the mapping between old-style and new-style zettel identifier.
type Mapper interface {
	Warnings(context.Context) (*id.Set, error) // Fetch problematic zettel identifier

	OldToNewMapping(ctx context.Context) (map[id.Zid]id.ZidN, error)
}

// 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:
90
91
92
93
94
95
96

97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

126
127
128
129
130
131
132







+




















-







	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys strfun.Set // Set of property key names
	zidMapper    *zidMapper

	// Indexer data
	idxLog   *logger.Logger
	idxStore store.Store
	idxAr    *anteroomQueue
	idxReady chan struct{} // Signal a non-empty anteroom to background task

	// Indexer stats data
	idxMx          sync.RWMutex
	idxLastReload  time.Time
	idxDurReload   time.Duration
	idxSinceReload uint64
}

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

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

139
140
141
142
143
144
145

146
147

148
149
150
151
152
153
154
147
148
149
150
151
152
153
154
155

156
157
158
159
160
161
162
163







+

-
+







		propertyKeys: propertyKeys,

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

	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged}
	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos, Mapper: mgr.zidMapper}
	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 {
211
212
213
214
215
216
217
218
219
220
221
222
223

224
225
226
227
228
229
230
220
221
222
223
224
225
226

227
228
229
230

231
232
233
234
235
236
237
238







-




-
+







				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 {
				if mgr.State() == box.StartStateStarted {
					mgr.notifyObserver(&ci)
				}
			}
		case <-mgr.done:
			return
		}
	}
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
253
254
255
256
257
258
259

260
261
262
263
264
265
266

267


268

269
270
271
272
273
274
275
276







-
+






-
+
-
-

-
+







	cache[zid] = destutterData{
		deadAt: now.Add(500 * time.Millisecond),
		reason: reason,
	}
	return false
}

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zidO id.Zid) {
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(zidO)
		mgr.idxAr.EnqueueZettel(zid)
	case box.OnDelete:
		mgr.idxAr.EnqueueZettel(zidO)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zidO).Msg("Unknown notification reason")
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
313
314
315
316
317
318
319

320
321
322
323
324
325
326







-







	}
	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() {
378
379
380
381
382
383
384
385

386
387
388
389
390
391
392
383
384
385
386
387
388
389

390
391
392
393
394
395
396
397







-
+








// 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}
	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()
428
429
430
431
432
433
434
435
436
437
438
439
440
433
434
435
436
437
438
439













-
-
-
-
-
-

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.

452
453
454
455
456
457
458






























































459
460
461
462
463
464
465
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







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







	if zi, ok := ms.idx[zid]; ok {
		return zi
	}
	zi := &zettelData{}
	ms.idx[zid] = zi
	return zi
}

func (ms *mapStore) 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.IUnion(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 *mapStore) copyDeadReferences(curDead *id.Set) *id.Set {
	// Must only be called if ms.mx is write-locked!
	curDead.ForEach(func(ref id.Zid) {
		ms.dead[ref] = ms.dead[ref].Add(ref)
	})
	return curDead.Clone()
}
func (ms *mapStore) copyForward(curForward *id.Set, newZid id.Zid) *id.Set {
	// Must only be called if ms.mx is write-locked!
	curForward.ForEach(func(ref id.Zid) {
		if fzi, found := ms.idx[ref]; found {
			fzi.backward = fzi.backward.Add(newZid)
		}

	})
	return curForward.Clone()
}
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] = msStringMap[s].Add(newZid)
		}
		return result
	}
	return nil
}

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

Changes to box/manager/store/store.go.

48
49
50
51
52
53
54




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







+
+
+
+








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

	// Optimize removes unneeded space.
	Optimize()

Added box/manager/zidmapper.go.








































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

import (
	"context"
	"maps"
	"sync"
	"time"

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

// zidMapper transforms old-style zettel identifier (14 digits) into new one (4 alphanums).
//
// Since there are no new-style identifier defined, there is only support for old-style
// identifier by checking, whether they are suported as new-style or not.
//
// This will change in later versions.
type zidMapper struct {
	fetcher   zidfetcher
	defined   map[id.Zid]id.ZidN // predefined mapping, constant after creation
	mx        sync.RWMutex       // protect toNew ... nextZidN
	toNew     map[id.Zid]id.ZidN // working mapping old->new
	toOld     map[id.ZidN]id.Zid // working mapping new->old
	nextZidM  id.ZidN            // next zid for manual
	hadManual bool
	nextZidN  id.ZidN // next zid for normal zettel
}

type zidfetcher interface {
	fetchZids(context.Context) (*id.Set, error)
}

// NewZidMapper creates a new ZipMapper.
func NewZidMapper(fetcher zidfetcher) *zidMapper {
	defined := map[id.Zid]id.ZidN{
		id.Invalid: id.InvalidN,
		1:          id.MustParseN("0001"), // ZidVersion
		2:          id.MustParseN("0002"), // ZidHost
		3:          id.MustParseN("0003"), // ZidOperatingSystem
		4:          id.MustParseN("0004"), // ZidLicense
		5:          id.MustParseN("0005"), // ZidAuthors
		6:          id.MustParseN("0006"), // ZidDependencies
		7:          id.MustParseN("0007"), // ZidLog
		8:          id.MustParseN("0008"), // ZidMemory
		9:          id.MustParseN("0009"), // ZidSx
		10:         id.MustParseN("000a"), // ZidHTTP
		11:         id.MustParseN("000b"), // ZidAPI
		12:         id.MustParseN("000c"), // ZidWebUI
		13:         id.MustParseN("000d"), // ZidConsole
		20:         id.MustParseN("000e"), // ZidBoxManager
		21:         id.MustParseN("000f"), // ZidZettel
		22:         id.MustParseN("000g"), // ZidIndex
		23:         id.MustParseN("000h"), // ZidQuery
		90:         id.MustParseN("000i"), // ZidMetadataKey
		92:         id.MustParseN("000j"), // ZidParser
		96:         id.MustParseN("000k"), // ZidStartupConfiguration
		100:        id.MustParseN("000l"), // ZidRuntimeConfiguration
		101:        id.MustParseN("000m"), // ZidDirectory
		102:        id.MustParseN("000n"), // ZidWarnings
		10100:      id.MustParseN("000s"), // Base HTML Template
		10200:      id.MustParseN("000t"), // Login Form Template
		10300:      id.MustParseN("000u"), // List Zettel Template
		10401:      id.MustParseN("000v"), // Detail Template
		10402:      id.MustParseN("000w"), // Info Template
		10403:      id.MustParseN("000x"), // Form Template
		10404:      id.MustParseN("001z"), // Rename Form Template (will be removed in the future)
		10405:      id.MustParseN("000y"), // Delete Template
		10700:      id.MustParseN("000z"), // Error Template
		19000:      id.MustParseN("000q"), // Sxn Start Code
		19990:      id.MustParseN("000r"), // Sxn Base Code
		20001:      id.MustParseN("0010"), // Base CSS
		25001:      id.MustParseN("0011"), // User CSS
		40001:      id.MustParseN("000o"), // Generic Emoji
		59900:      id.MustParseN("000p"), // Sxn Prelude
		60010:      id.MustParseN("0012"), // zettel
		60020:      id.MustParseN("0013"), // confguration
		60030:      id.MustParseN("0014"), // role
		60040:      id.MustParseN("0015"), // tag
		90000:      id.MustParseN("0016"), // New Menu
		90001:      id.MustParseN("0017"), // New Zettel
		90002:      id.MustParseN("0018"), // New User
		90003:      id.MustParseN("0019"), // New Tag
		90004:      id.MustParseN("001a"), // New Role
		// 100000000,   // Manual               -> 0020-00yz
		9999999997:  id.MustParseN("00zx"), // ZidSession
		9999999998:  id.MustParseN("00zy"), // ZidAppDirectory
		9999999999:  id.MustParseN("00zz"), // ZidMapping
		10000000000: id.MustParseN("0100"), // ZidDefaultHome
	}
	toNew := maps.Clone(defined)
	toOld := make(map[id.ZidN]id.Zid, len(toNew))
	for o, n := range toNew {
		if _, found := toOld[n]; found {
			panic("duplicate predefined zid")
		}
		toOld[n] = o
	}

	return &zidMapper{
		fetcher:   fetcher,
		defined:   defined,
		toNew:     toNew,
		toOld:     toOld,
		nextZidM:  id.MustParseN("0020"),
		hadManual: false,
		nextZidN:  id.MustParseN("0101"),
	}
}

// isWellDefined returns true, if the given zettel identifier is predefined
// (as stated in the manual), or is part of the manual itself, or is greater than
// 19699999999999.
func (zm *zidMapper) isWellDefined(zid id.Zid) bool {
	if _, found := zm.defined[zid]; found || (1000000000 <= zid && zid <= 1099999999) {
		return true
	}
	if _, err := time.Parse("20060102150405", zid.String()); err != nil {
		return false
	}
	return 19700000000000 <= zid
}

// Warnings returns all zettel identifier with warnings.
func (zm *zidMapper) Warnings(ctx context.Context) (*id.Set, error) {
	allZids, err := zm.fetcher.fetchZids(ctx)
	if err != nil {
		return nil, err
	}
	warnings := id.NewSet()
	allZids.ForEach(func(zid id.Zid) {
		if !zm.isWellDefined(zid) {
			warnings = warnings.Add(zid)
		}
	})
	return warnings, nil
}

func (zm *zidMapper) GetZidN(zidO id.Zid) id.ZidN {
	zm.mx.RLock()
	if zidN, found := zm.toNew[zidO]; found {
		zm.mx.RUnlock()
		return zidN
	}
	zm.mx.RUnlock()

	zm.mx.Lock()
	defer zm.mx.Unlock()
	// Double check to avoid races
	if zidN, found := zm.toNew[zidO]; found {
		return zidN
	}

	if 1000000000 <= zidO && zidO <= 1099999999 {
		if zidO == 1000000000 {
			zm.hadManual = true
		}
		if zm.hadManual {
			zidN := zm.nextZidM
			zm.nextZidM++
			zm.toNew[zidO] = zidN
			zm.toOld[zidN] = zidO
			return zidN
		}
	}

	zidN := zm.nextZidN
	zm.nextZidN++
	zm.toNew[zidO] = zidN
	zm.toOld[zidN] = zidO
	return zidN
}

// OldToNewMapping returns the mapping of old format identifier to new format identifier.
func (zm *zidMapper) OldToNewMapping(ctx context.Context) (map[id.Zid]id.ZidN, error) {
	allZids, err := zm.fetcher.fetchZids(ctx)
	if err != nil {
		return nil, err
	}

	result := make(map[id.Zid]id.ZidN, allZids.Length())
	allZids.ForEach(func(zidO id.Zid) {
		zidN := zm.GetZidN(zidO)
		result[zidO] = zidN
	})
	return result, nil
}

Changes to box/membox/membox.go.

50
51
52
53
54
55
56
57
58
59



60
61
62
63
64
65
66
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) 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()
}

112
113
114
115
116
117
118
119

120
121
122
123
124
125
126
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.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]
197
198
199
200
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
197
198
199
200
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







-
+



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


















-
+











		return box.ErrCapacity
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid, box.OnZettel)
	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, box.OnDelete)
	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")
}

Changes to box/notify/directory.go.

14
15
16
17
18
19
20

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







+







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

61
62
63
64
65
66
67
68
69
70

71
72
73
74
75

76
77
78
79
80
81
82
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
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
	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, notify box.UpdateNotifier) *DirService {
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    notify,
		infos:    chci,
		state:    DsCreated,
	}
}

// State the current service state.
func (ds *DirService) State() DirServiceState {
	ds.mx.RLock()
181
182
183
184
185
186
187






























188
189
190
191
192
193
194
181
182
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







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







	defer ds.mx.Unlock()
	if ds.entries == nil {
		return ds.logMissingEntry("update")
	}
	ds.entries[entry.Zid] = &entry
	return nil
}

// RenameDirEntry replaces an existing directory entry with a new one.
func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return DirEntry{}, ds.logMissingEntry("rename")
	}
	if _, found := ds.entries[newZid]; found {
		return DirEntry{}, box.ErrInvalidZid{Zid: newZid.String()}
	}
	oldZid := oldEntry.Zid
	newEntry := DirEntry{
		Zid:         newZid,
		MetaName:    renameFilename(oldEntry.MetaName, oldZid, newZid),
		ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid),
		ContentExt:  oldEntry.ContentExt,
		// Duplicates must not be set, because duplicates will be deleted
	}
	delete(ds.entries, oldZid)
	ds.entries[newZid] = &newEntry
	return newEntry, nil
}

func renameFilename(name string, curID, newID id.Zid) string {
	if cur := curID.String(); strings.HasPrefix(name, cur) {
		name = newID.String() + name[len(cur):]
	}
	return name
}

// DeleteDirEntry removes a entry from the directory.
func (ds *DirService) DeleteDirEntry(zid id.Zid) error {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return ds.logMissingEntry("delete")
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
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, box.OnZettel)
			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, box.OnDelete)
			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, box.OnZettel)
		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, box.OnDelete)
		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, box.OnDelete)
		ds.notifyChange(zid)
	}
}

var validFileName = regexp.MustCompile(`^(\d{14})`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
571
572
573
574
575
576
577
578
579
580
581




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

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
71
72
73
74
75
76
77
78
79
80
81
82
83
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))
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135
136
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) }

Changes to cmd/main.go.

158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

175
176
177
178
179
180
181
182
183
158
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"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyInsecureHTML      = "insecure-html"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyReadOnly          = "read-only-mode"
	keyRuntimeProfiling  = "runtime-profiling"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) bool {
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
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.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))

	err = setConfigValue(
		err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	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
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
var version string = ""

func main() {
	exitCode := cmd.Main("Zettelstore", version)
	os.Exit(exitCode)
}

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

Changes to docs/development/20210916193200.zettel.

21
22
23
24
25
26
27
28
29
21
22
23
24
25
26
27

28







-

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``,
* [[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/20231218181900.zettel.

67
68
69
70
71
72
73

74
75
76
77
78
79
80
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

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
1
2
3
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
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}}
Version: {{00001000000001}}.

Licensed under the EUPL-1.2-or-later.

Changes to docs/manual/00001004010000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240926144803
modified: 20240710183532

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.

46
47
48
49
50
51
52
53

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

53
54
55

56
57
58
59
60
61
62







-
+


-







: 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).
: If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by the developers).
  Disables any timeout values of the internal web server and does not send some security-related data.
  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.

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





123
124
125
126
127
128
129







-
-
-
-
-








  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.

Deleted 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/00001004020000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241118175216
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]].
56
57
58
59
60
61
62
63

64
65

66
67
68
69
70
71
72
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'']
; [!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]], [[''sequel''|00001006020000#sequel]], and  [[''prequel''|00001006020000#prequel]].
  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: """".

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
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
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-sequel-links''|00001004020000#show-sequel-links]]|Y|Y|
|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y|
|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y|

Changes to docs/manual/00001005000000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







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

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

33
34

35
36

37
38
39
40
41
42
43
25
26
27
28
29
30
31

32
33

34
35

36
37
38
39
40
41
42
43







-
+

-
+

-
+







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.
This allows zettel to be sorted naturally by creation time.[^Zettel identifier format will be migrated to a new format after version 0.19, without reference to the creation date. See [[Alphanumeric Zettel Identifier|00001006050200]] for some details.]

Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.
Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.[^Zettel identifier format will be migrated to a new format after version 0.19, without reference to the creation date.]
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.
You can create these special zettel identifiers either with the __rename__[^Renaming is deprecated als will be removed in version 0.19 or after.] 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;
70
71
72
73
74
75
76
77

78
79
80
81
82
83
84
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).
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[^Renaming is deprecated als will be removed in version 0.19 or after.] 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.]

Changes to docs/manual/00001005090000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







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

The following table lists all predefined zettel with their purpose.[^Zettel identifier format will be migrated to a new format after version 0.19.]

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

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







+







| [[00000000000102]] | Zettelstore Warnings | Warnings about potential problematic zettel identifier
| [[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

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241118175033
modified: 20240708154737

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'']
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
85
86
87
88
89
90
91



92
93
94
95
96
97
98







-
-
-







; [!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.

110
111
112
113
114
115
116
117
118


119
120
121
122
123
124
125
126
127



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

146
147
148
149
150
151
152
107
108
109
110
111
112
113


114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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.
; [!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 deleted, these files will also be deleted.
  If a zettel is renamed[^Renaming a zettel is deprecated. This feature will be removed in version 0.19 or later.] 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.

Changes to docs/manual/00001006050000.zettel.

1
2
3
4
5
6
7

8
9
10
11


12
13
14
15

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











1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16

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






-
+




+
+



-
+















+
+
+
+
+
+
+
+
+
+
+
id: 00001006050000
title: Zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241128141443
modified: 20240708154551

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.

=== Timestamp-based identifier

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the
next two represent the month, following by day, hour, minute, and second.
next two represent the month, following by day, hour, minute, and second.[^Zettel identifier format will be migrated to a new format after version 0.19, without reference to the creation date.]

This allows to order zettel chronologically in a canonical way.

In most cases the zettel identifier is the timestamp when the zettel was created.

However, the Zettelstore software just checks for exactly 14 digits.
Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with
a month part of ""35"" or with ""99"" as the last two digits.

Some zettel identifier are [[reserved|00001006055000]] and should not be used otherwise.
All identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''.
Zettel identifier of this manual have be chosen to begin with ""000010"".

A zettel can have any identifier that contains 14 digits and that is not in use
by another zettel managed by the same Zettelstore.

=== Identifiers with four alphanumeric characters
In the future, above identifier format will change.
The migration to the new format starts with Zettelstore version 0.18 and will last approximately until version 0.22.

Above described format of 14 digits will be changed to four alphanumeric characters, i.e. the digits ''0'' to ''9'', and the letters ''a'' to ''z''.
You might note that using 14 digits you are allowed a little less than 10^^14^^ Zettel, i.e. more than 999 trillion zettel, while the new scheme only allows you to create 36^^4^^-1 zettel (1679615 zettel, to be exact).
Since Zettelstore is a single-user system, more than a million zettel should be enough.
However, there must be a way to replace an identifier with 14 digits by an identifier with four characters.

As a first step, the list of [[reserved zettel identifier|00001006055000]] is updated, as well as ways of client software to use predefined identifier.

Added docs/manual/00001006050200.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: 00001006050200
title: Alphanumeric Zettel Identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20240705200557
modified: 20240710173133
precursor: 00001006050000

Timestamp-based zettel identifier (14 digits) will be migrated to a new format.
Instead of using the current date and time of zettel creation, the new format is based in incrementing zettel identifier.
When creating a new zettel, its identifier is calculated by adding one to the current maximum zettel identifier.
The external representation if the new format identifier is a sequence of four alphanumeric characters, i.e. the 36
characters ''0'' &hellip; ''9'', and ''a'' &hellip; ''z''.
The external representation is basically a ""base-36"" encoding of the number.

The characters ''A'' &hellip; ''Z'' are mapped to the lower-case ''a'' &hellip; ''z''.

=== Migration process
Please note: the following is just a plan.
Plans tend to be revised if they get in contact with reality.

; Version 0.18
: Provides some tools to check your own zettelstore for problematic zettel identifier.
  For example, zettel without metadata key ''created'' should be updated by the user, especially if the zettel identifier is below ''19700101000000''.
  Most likely, this is the case for zettel created before version 0.7 (2022-08-17).

  Zettel [[Zettelstore Warnings|00000000000102]] (''00000000000102'') lists these problematic zettel identifier.[^Only visible in [[expert mode|00001004020000#expert-mode]].]
  You should update your zettel to remove these warnings to ensure a smooth migration.

  If you have developed an application, that defines a specific zettel identifier to be used as application configuration, you should must the new zettel [[Zettelstore Application Directory|00009999999998]] (''00009999999998'').

  There is an explicit, but preliminary mapping of the old format to the new one, and vice versa.
  This mapping will be calculated with the order of the identifier in the old format.
  The zettel [[Zettelstore Identifier Mapping|00009999999999]] (''00009999999999'') will show this mapping.[^Only visible in [[expert mode|00001004020000#expert-mode]].]

; Version 0.19
: The new identifier format will be used initially internal.
  The old format with 14 digits is still used to create URIs and to link zettel.

  You will have some time to update your zettel data if you detect some issues.

  Operation to rename a zettel, i.e. assigning a new identifier to a zettel, is remove permanently.
; Version 0.20
: The internal search index is based on the new format identifier.
; Version 0.21
: The new format is used to calculate URIs and to form links.
; Version 0.22
: Old format identifier are full legacy.

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

7
8
9

10
11
12
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: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210721105704
modified: 20241128141327
modified: 20240708154858

[[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.
By renaming[^The rename operation id deprecated and will be removed in version 0.19 or later.] a zettel, you are able to provide any sequence of 14 digits[^Zettel identifier format will be migrated to a new format after version 0.19.].
If no other zettel has the same identifier, you are allowed to rename a zettel.

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|00009999999998]] (''00009999999998'') 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 | 0000000000000 | This is an invalid zettel identifier
| 00000000000001 | 0000099999999 | [[Predefined zettel|00001005090000]]
| 00001000000000 | 0000109999999 | This [[Zettelstore manual|00001000000000]]
| 00001100000000 | 0000899999999 | Reserved, do not use
| 00009000000000 | 0000999999999 | Reserved for applications
| 00001100000000 | 0000899999999 | Reserved, do not use.
| 00009000000000 | 0000999999999 | Reserved for applications (legacy)

Since the format of zettel identifier will change in the near future, no external application is allowed to use the range ''00000000000001'' &hellip; ''0000999999999''.

==== External Applications
==== External Applications (Legacy)
|= 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/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
1
2
3
4
5
6

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

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
1
2
3
4
5
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
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]]
| ''+''  | (reserved) | (reserved)
| ''+''  | (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)

Changes to docs/manual/00001010000000.zettel.

1
2
3
4
5
6
7

8
9
10


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

7
8


9
10
11
12
13
14
15
16
17






-
+

-
-
+
+







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

Your zettel may contain sensitive content.
You probably want to ensure that only authorized persons can read and/or modify them.
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.

Changes to docs/manual/00001010070600.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







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

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.
39
40
41
42
43
44
45



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







+
+
+




** 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[^Renaming is deprecated. This operation will be removed in version 0.19 or later.]
** Reject the access.
   Only the owner of the Zettelstore is currently allowed to give a new identifier for a zettel.
* Delete a zettel
** Reject the access.
   Only the owner of the Zettelstore is allowed to delete a zettel.
   This may change in the future.

Changes to docs/manual/00001012000000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240711183736
modified: 20240708154140

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
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]] (deprecated)
* [[Delete a zettel|00001012054600]]

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

Added docs/manual/00001012054400.zettel.


























































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012054400
title: API: Rename a zettel
role: manual
tags: #api #manual #zettelstore #deprecated
syntax: zmk
created: 20210713150005
modified: 20240708154151

**Note:** this operation is deprecated and will be removed in version 0.19 (or later).
Do not use it anymore.

If your client application depends on this operation, please get in contact with the [[author/maintainer|00000000000005]] of Zettelstore to find a solution.

---
**Deprecated**

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

7
8
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: 20240711183819
modified: 20240708155042

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]][^Renaming a zettel is deprecated and will be removed in version 0.19 or later.]

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

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
modified: 20240708155122

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)
| 4 | 16 | User is allowed to rename the zettel[^Renaming a zettel is deprecated and will be removed in version 0.19 or later.]
| 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/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
1
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: 20241128142457
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 allow to start Zettelstore.
** **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.
50
51
52
53
54
55
56
57
58
59
60
61
62
50
51
52
53
54
55
56













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

Changes to encoder/encoder_block_test.go.

329
330
331
332
333
334
335
336

337
338
339

340
341
342
343
344
345
346
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>`,
			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 (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")))))`,
			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`,
		},
	},
	{

Changes to encoder/encoder_inline_test.go.

147
148
149
150
151
152
153
154

155
156
157
158
159
160
161
162
163
164
165
166

167
168
169
170
171
172
173
174
175
176
177
178

179
180
181
182
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
147
148
149
150
151
152
153

154
155
156
157
158
159
160
161
162
163
164
165

166
167
168
169
170
171
172
173
174
175
176
177

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







-
+











-
+











-
+











-
+











-
+











-
+







		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderHTML:  "&ldquo;quotes&rdquo;",
			encoderMD:    "&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:    "&bdquo;quotes&ldquo;",
			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:    "&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:    "&quot;&quot;",
			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:    `&ldquo;say: &lsquo;yes, &ldquo;or?&rdquo;&rsquo;&rdquo;`,
			encoderMD:    "<q>say: <q>yes, <q>or?</q></q></q>",
			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;`,
			encoderMD:    "<q>yes</q> or <q>no</q>",
			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,
		},
	},
	{
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
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: "Input formatting",
		zmk:   `''input''`,
		expect: expectMap{
			encoderHTML:  `<kbd>input</kbd>`,
			encoderMD:    "`input`",
			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`",
			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
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;",
			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"}`,
		},
	},
	{
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
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>`,
			encoderHTML:  `<a class="external" href="abc">abc</a>`,
			encoderMD:    "[abc](abc)",
			encoderSz:    `(INLINE (LINK-EXTERNAL () "abc"))`,
			encoderSHTML: `((a (@ (href . "abc") (rel . "external")) "abc"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "abc")) "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>`,
			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 (@ (href . "https://zettelstore.de") (rel . "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 href="https://zettelstore.de" rel="external">Home</a>`,
			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 (@ (href . "https://zettelstore.de") (rel . "external")) "Home"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "Home"))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,

Changes to encoder/htmlenc/htmlenc.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
27
28
29
30
31
32
33



34

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

49
50
51
52
53
54
55







-
-
-
+
-














-







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

78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
74
75
76
77
78
79
80

81
82
83
84
85
86
87
88







-
+







	}

	xast := he.tx.GetSz(&zn.Ast)
	hast, err := he.th.Evaluate(xast, &env)
	if err != nil {
		return 0, err
	}
	hen := shtml.Endnotes(&env)
	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.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta))
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

147
148
149
150
151
152
153
119
120
121
122
123
124
125

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

141
142
143
144
145
146
147
148







-















-
+







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

Changes to encoder/mdenc/mdenc.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
14
15
16
17
18
19
20


21
22
23
24
25
26



27

28
29
30

31


32



33


34
35
36
37


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


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



73
74
75
76
77


78
79
80
81
82
83
84
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"

	"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) },
	encoder.Register(api.EncoderMD, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
	)
}

// Create an encoder.
func Create(params *encoder.CreateParameter) *Encoder {
func Create() *Encoder { return &myME }
	return &Encoder{lang: params.Lang}
}


// Encoder contains all data needed for encoding.
type Encoder struct {
type Encoder struct{}
	lang string
}

var myME Encoder

// 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)
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 (me *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, me.lang)
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')
	}
}

// WriteContent encodes the zettel content.
func (me *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return me.WriteBlocks(w, &zn.Ast)
func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (me *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w, me.lang)
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 (me *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w, me.lang)
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
	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)}
func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewEncWriter(w)}
}

// 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:
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
177
178
179
180
181
182
183



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199



200
201
202
203
204
205
206







-
-
-
















-
-
-







	}
}

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 {
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
277
278
279
280
281
282
283



284
285
286
287



288
289
290
291
292
293
294







-
-
-




-
-
-







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







-
-
-










-
+
+
+









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


-
+







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

Changes to encoder/shtmlenc/shtmlenc.go.

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

44
45
46
47
48
49
50







-







	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.
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
68
69
70
71
72
73
74

75
76
77
78
79
80
81







-







	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.

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

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

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







-
















-













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

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

35
36
37
38

39
40
41
42
43
44
45







-




-








// 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:
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
357
358
359
360
361
362
363

364
365
366
367
368
369
370







-







	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)

Changes to encoder/textenc/textenc.go.

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

34
35
36
37
38
39
40







-







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)
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
69
70
71
72
73
74
75

76
77
78
79
80
81
82







-







			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)

Changes to encoder/zmkenc/zmkenc.go.

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

38
39
40
41
42
43
44







-







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)
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
71
72
73
74
75
76
77

78
79
80
81
82
83
84







-







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

Changes to encoding/atom/atom.go.

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
25
26
27
28
29
30
31

32
33

34
35
36
37
38
39

40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56







-


-






-










-







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

// ContentType specifies the HTTP content type for Atom.
const ContentType = "application/atom+xml"

// Configuration contains data to configure the Atom encoding.
type Configuration struct {
	Title            string
	Generator        string
	NewURLBuilderAbs func() *api.URLBuilder
}

// Setup initializes the Configuration.
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') }
}

// Marshal encodes the result of a query as Atom.
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)

Changes to encoding/rss/rss.go.

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

33
34

35
36
37
38
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/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ContentType specifies the HTTP content type for RSS.
const ContentType = "application/rss+xml"

// Configuration contains data to configure the RSS encoding.
type Configuration struct {
	Title            string
	Language         string
	Copyright        string
	Generator        string
	NewURLBuilderAbs func() *api.URLBuilder
}

// Setup initializes the Configuration.
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') }
}

// Marshal encodes the result of a query as Atom.
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()
	}

Changes to evaluator/list.go.

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







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

60
61
62
63
64
65

66
67
68
69
70
71
72
31
32
33
34
35
36
37







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

59
60
61
62
63
64

65
66
67
68
69
70
71
72







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














-
+





-
+







	"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,
		minVal: -1,
		maxVal: -1,
		title:  rtConfig.GetSiteName(),
		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.minVal = num
				ap.min = num
				continue
			}
		}
		if strings.HasPrefix(act, api.MaxAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.maxVal = num
				ap.max = num
				continue
			}
		}
		if act == api.TitleAction && i+1 < len(actions) {
			ap.title = strings.Join(actions[i+1:], " ")
			break
		}
100
101
102
103
104
105
106
107
108
109
110
111
112
113







114
115
116
117
118
119
120
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
	minVal int
	maxVal int
	title  string
	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
170
171
172
173
174
175
176
177
178
179



180
181
182


183
184

185
186
187

188
189
190
191
192
193
194
170
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 minVal, maxVal := ap.minVal, ap.maxVal; minVal > 0 || maxVal > 0 {
		if minVal < 0 {
			minVal = ccs[len(ccs)-1].Count
	if min, max := ap.min, ap.max; min > 0 || max > 0 {
		if min < 0 {
			min = ccs[len(ccs)-1].Count
		}
		if maxVal < 0 {
			maxVal = ccs[0].Count
		if max < 0 {
			max = ccs[0].Count
		}
		if ccs[len(ccs)-1].Count < minVal || maxVal < 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 minVal <= cat.Count && cat.Count <= maxVal {
				if min <= cat.Count && cat.Count <= max {
					temp = append(temp, cat)
				}
			}
			return temp
		}
	}
	return ccs

Changes to go.mod.

1
2
3

4
5
6
7
8
9
10
11
12
13








14
15
16
17
18


19
1
2

3
4
5








6
7
8
9
10
11
12
13
14
15
16


17
18
19


-
+


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



-
-
+
+

module zettelstore.de/z

go 1.23
go 1.22

require (
	github.com/fsnotify/fsnotify v1.8.0
	github.com/yuin/goldmark v1.7.8
	golang.org/x/crypto v0.29.0
	golang.org/x/term v0.26.0
	golang.org/x/text v0.20.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-20241127165008-6a850fba0141
	github.com/fsnotify/fsnotify v1.7.0
	github.com/yuin/goldmark v1.7.4
	golang.org/x/crypto v0.25.0
	golang.org/x/term v0.22.0
	golang.org/x/text v0.16.0
	t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca
	t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245
	t73f.de/r/zsc v0.0.0-20240711144034-b141c81ad9b7
)

require (
	golang.org/x/sys v0.27.0 // indirect
	t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd // indirect
	golang.org/x/sys v0.22.0 // indirect
	t73f.de/r/webs v0.0.0-20240617100047-8730e9917915 // indirect
)

Changes to go.sum.

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








































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
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-20241127165008-6a850fba0141 h1:OiekawDNkf/3m0cYAmFi7A69xIMdUjMzQIlue2a61MQ=
t73f.de/r/zsc v0.0.0-20241127165008-6a850fba0141/go.mod h1:PWnU0AvNxVumQiQBMBr9GeGTaAv8ZD78voHaPIs0omI=
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.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca h1:vvDqiuUfBLf+t/gpiSyqIFAdvZ7FLigOH38bqMY+v8k=
t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca/go.mod h1:G9pD1j2R6y9ZkPBb81mSnmwaAvTOg7r6jKp/OF7WeFA=
t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245 h1:raE7KUgoGsp2DzXOko9dDXEsSJ/VvoXCDYeICx7i6uo=
t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245/go.mod h1:ErPBVUyE2fOktL/8M7lp/PR93wP/o9RawMajB1uSqj8=
t73f.de/r/webs v0.0.0-20240617100047-8730e9917915 h1:rwUaPBIH3shrUIkmw51f4RyCplsCU+ISZHailsLiHTE=
t73f.de/r/webs v0.0.0-20240617100047-8730e9917915/go.mod h1:UGAAtul0TK5ACeZ6zTS3SX6GqwMFXxlUpHiV8oqNq5w=
t73f.de/r/zsc v0.0.0-20240711144034-b141c81ad9b7 h1:Ysb9nud8uhB4N1hUMW3GmFvWabo1r6UlcG/DhhubyCQ=
t73f.de/r/zsc v0.0.0-20240711144034-b141c81ad9b7/go.mod h1:FH9nouOzCHoR0Nbk6gBK31gGJqQI8dGVXoyGI45yHkM=

Changes to kernel/impl/cfg.go.

92
93
94
95
96
97
98
99
100
101
102
103





104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

















123
124
125
126
127
128
129
92
93
94
95
96
97
98





99
100
101
102
103
104
105

















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},
		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.KeyShowSequelLinks:    "",
		config.KeyShowSuccessorLinks: "",
		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)
180
181
182
183
184
185
186
187

188
189
190
191
192
193
194
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 {
	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)

Changes to kernel/impl/cmd.go.

140
141
142
143
144
145
146
147

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

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197

198
199
200
201
202
203
204
140
141
142
143
144
145
146

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

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

177
178
179
180
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 {
		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, _ string, _ []string) bool {
		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, _ string, _ []string) bool {
		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, _ string, _ []string) bool { sess.kern.Shutdown(false); return false },
		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 {
337
338
339
340
341
342
343
344
345







346
347
348
349
350
351
352
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 {
	if srvnum, ok := lookupService(sess, cmd, args); ok {
		sess.kern.doStopService(srvnum)
	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")

Changes to kernel/impl/config.go.

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
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 u64, err := strconv.ParseInt(val, 10, 64); err == nil {
	if err == nil {
		return u64, nil
	}
	return nil, err
	} else {
		return nil, err
	}
}

func parseZid(val string) (any, error) {
	zid, err := id.Parse(val)
	if zid, err := id.Parse(val); err == nil {
	if err == nil {
		return zid, nil
	}
	return id.Invalid, err
	} else {
		return id.Invalid, err
	}
}

func parseInvalidZid(val string) (any, error) {
	zid, _ := id.Parse(val)
	return zid, nil
}

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
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) {
func (kern *myKernel) StopService(srvnum kernel.Service) error {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	kern.doStopService(srvnum)
	return kern.doStopService(srvnum)
}

func (kern *myKernel) doStopService(srvnum kernel.Service) {
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 {
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567














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

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
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() {
				if finfo, err := os.Stat(val); err == nil && finfo.IsDir() {
					return val, nil
				}
				return nil, err
				} else {
					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
92
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.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: {
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
107
108
109
110
111
112
113

114
115
116
117
118
119
120







-







	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 {
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175

176
177
178
179
180
181
182
139
140
141
142
143
144
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)
	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)
	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()

Changes to kernel/kernel.go.

90
91
92
93
94
95
96
97

98
99
100
101
102
103
104
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)
	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)

189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
189
190
191
192
193
194
195

196
197
198
199
200
201
202







-








// 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 parser/markdown/markdown.go.

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







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




-
+



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







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

Changes to parser/plain/plain.go.

12
13
14
15
16
17
18

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







+







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

// Package plain provides a parser for plain text data.
package plain

import (
	"bytes"
	"strings"

	"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"
120
121
122
123
124
125
126
127
128
129





130
131
132
133
134

135
136
137

138
139
140
141
142
143
144
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 {
	inp.SkipSpace()
	pos := inp.Pos
	if !inp.Accept("<svg") {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	}
	svgSrc := string(inp.Src[inp.Pos:])
	if !strings.HasPrefix(svgSrc, "<svg ") {
		return ""
	}
	ch := inp.Ch
	if input.IsSpace(ch) || input.IsEOLEOS(ch) || ch == '>' {
		// TODO: check proper end </svg>
	// TODO: check proper end </svg>
		return string(inp.Src[pos:])
	}
	return ""
	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{

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

284
285
286
287
288
289
290
291
292
293

294
295

296
297
298
299
300
301
302
284
285
286
287
288
289
290

291

292
293

294
295
296
297
298
299
300
301







-

-
+

-
+







		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()
	cp.skipSpace()
	for {
		switch inp.Ch {
		switch cp.inp.Ch {
		case input.EOS, '\n', '\r':
			return
		}
		in := cp.parseInline()
		if in == nil {
			return
		}
311
312
313
314
315
316
317
318

319
320
321
322
323
324
325
310
311
312
313
314
315
316

317
318
319
320
321
322
323
324







-
+







	if delims < 3 {
		return nil, false
	}
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	cp.skipSpace()
	if delims > 7 {
		delims = 7
	}
	hn = &ast.HeadingNode{Level: delims - 2, Inlines: nil}
	for {
		if input.IsEOLEOS(inp.Ch) {
			return hn, true
358
359
360
361
362
363
364
365

366
367
368
369
370
371
372
357
358
359
360
361
362
363

364
365
366
367
368
369
370
371







-
+







// 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()
	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)]
	}
443
444
445
446
447
448
449
450

451
452
453
454
455
456
457
442
443
444
445
446
447
448

449
450
451
452
453
454
455
456







-
+







func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	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
477
478
479
480
481
482
483
484

485
486
487
488
489
490
491
476
477
478
479
480
481
482

483
484
485
486
487
488
489
490







-
+







func (cp *zmkP) parseDefDescr() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	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

Changes to parser/zettelmark/inline.go.

157
158
159
160
161
162
163
164

165
166
167
168
169
170
171
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()
	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)
190
191
192
193
194
195
196
197

198
199
200
201
202
203
204
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()
	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 {
309
310
311
312
313
314
315
316
317

318

319
320
321
322
323
324
325
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()
	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) {
380
381
382
383
384
385
386
387

388
389
390
391
392
393
394
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()
	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]...),

Changes to parser/zettelmark/zettelmark.go.

152
153
154
155
156
157
158
159

160
161
162
163
164
165
166
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()
	cp.skipSpace()
	return cp.parseInlineAttributes()
}

func (cp *zmkP) parseInlineAttributes() attrs.Attributes {
	inp := cp.inp
	pos := inp.Pos
	if attrs, success := cp.doParseAttributes(); success {
235
236
237
238
239
240
241






242
243
244
245
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251







+
+
+
+
+
+




		case '\n', '\r':
			inp.EatEOL()
		default:
			return
		}
	}
}

func (cp *zmkP) skipSpace() {
	for inp := cp.inp; inp.Ch == ' '; {
		inp.Next()
	}
}

func isNameRune(ch rune) bool {
	return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_'
}

Changes to query/context.go.

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
30
31
32
33
34
35
36

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

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

68
69
70
71
72
73
74







-












-



















-







	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 {
178
179
180
181
182
183
184
185
186
187



188
189
190
191
192
193
194
175
176
177
178
179
180
181



182
183
184
185
186
187
188
189
190
191







-
-
-
+
+
+







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

Changes to query/parser.go.

41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

79
80
81

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

97
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112
41
42
43
44
45
46
47


48
49
50
51
52
53



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


75
76
77
78
79
80
81
82
83
84
85
86
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 {
	inp := ps.inp
	if inp.Accept(s) && (inp.IsSpace() || ps.isActionSep() || ps.mustStop()) {
	if ps.inp.Accept(s) && (ps.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()
	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 {
	inp := ps.inp
	inp.SkipSpace()
	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.Contains(zid) {
			zidSet.Add(zid)
			q = createIfNeeded(q)
			q.zids = append(q.zids, zid)
		}
		inp.SkipSpace()
		ps.skipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
		}
	}

	hasContext := false
	for {
		inp.SkipSpace()
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {
			if hasContext {
				inp.SetPos(pos)
139
140
141
142
143
144
145
146

147
148
149
150
151
152
153
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 {
		inp.SkipSpace()
		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() {
201
202
203
204
205
206
207
208

209
210
211
212
213
214
215
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 {
		inp.SkipSpace()
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.FullDirective) {
			spec.Full = true
			continue
266
267
268
269
270
271
272
273

274
275
276
277
278
279
280
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 {
		inp.SkipSpace()
		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))
342
343
344
345
346
347
348
349
350

351
352
353

354
355
356
357
358
359
360
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 {
	inp := ps.inp
	inp.Next()
	ps.inp.Next()
	var words []string
	for {
		inp.SkipSpace()
		ps.skipSpace()
		word := ps.scanWord()
		if len(word) == 0 {
			break
		}
		words = append(words, string(word))
	}
	if len(words) > 0 {
374
375
376
377
378
379
380
381

382
383
384
385
386
387
388
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 inp.IsSpace() || ps.isActionSep() || ps.mustStop() {
			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 {
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
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 !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() {
	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 !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() {
	for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() {
		inp.Next()
	}
	return inp.Src[pos:inp.Pos]
}

func (ps *parserState) scanPosInt() (int, bool) {
	word := ps.scanWord()
510
511
512
513
514
515
516

















517
518
519
520
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







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




		return cmpUnknown, false
	}
	if negate {
		return op.negate(), true
	}
	return op, true
}

func (ps *parserState) skipSpace() {
	for ps.isSpace() {
		ps.inp.Next()
	}
}

func (ps *parserState) isSpace() bool {
	switch ch := ps.inp.Ch; ch {
	case input.EOS:
		return false
	case ' ', '\t', '\n', '\r':
		return true
	default:
		return input.IsSpace(ch)
	}
}

func (ps *parserState) isActionSep() bool {
	return ps.inp.Ch == actionSeparatorChar
}

Changes to query/print.go.

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

152
153
154
155
156
157
158







-







		}
		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/specs.go.

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

21
22
23
24
25
26
27
28

29
30
31
32







-








-




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.

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

27
28
29
30
31
32
33
34

35
36
37
38
39
40
41







-








-







)

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

50
51
52
53
54
55
56
57
58
59
60




61
62
63
64
65
66
67
50
51
52
53
54
55
56




57
58
59
60
61
62
63
64
65
66
67







-
-
-
-
+
+
+
+







		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 57
		configRoleZettel = 35
		writerZettel     = ownerZettel - 25
		readerZettel     = ownerZettel - 25
		ownerZettel      = 60
		configRoleZettel = 38
		writerZettel     = ownerZettel - 28
		readerZettel     = ownerZettel - 28
		creatorZettel    = 10
		publicZettel     = 5
	)

	testdata := []struct {
		user string
		exp  int

Changes to tests/client/crud_test.go.

21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35







-
+







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





55
56



57
58
59

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






76


77
78

79
80
81
82
83
84
85
48
49
50
51
52
53
54
55
56
57
58
59


60
61
62
63
64

65
66
67
68
69
70
71
72
73
74
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, zid)
	}

	doDelete(t, c, newZid)
}

func TestCreateGetDeleteZettelDataCreator(t *testing.T) {
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, zid)
	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"

Changes to tests/markdown_test.go.

84
85
86
87
88
89
90
91

92
93
94
95
96
97
98
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) {
		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) {

Changes to tools/build/build.go.

245
246
247
248
249
250
251
252
253


254
255
256
257
258
259
260
261
262
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"},
		{"arm64", "darwin", nil, "zettelstore"},
		{"amd64", "darwin", nil, "zettelstore"},
		{"amd64", "darwin", nil, "zettelstore"},
		{"arm64", "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
49
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"},
		{"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
1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20













-







//-----------------------------------------------------------------------------
// 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"
58
59
60
61
62
63
64
65

66
67
68
69
70
71
72
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71







-
+







		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++
				msgCount += 1
			} else {
				msgCount += nmsgs
			}
		}
		if msgCount == 1 {
			fmt.Fprintln(os.Stderr, "==> found 1 possible issue")
		} else if msgCount > 1 {
106
107
108
109
110
111
112

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







+







	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 {

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
22
23
24
25
26
27
28




29
30



31
32

33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57
58
59


60
61
62
63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
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"
)

// Some constants to make Go work with fossil.
var (
	EnvDirectProxy = []string{"GOPROXY=direct"}
	EnvGoVCS       = []string{"GOVCS=zettelstore.de:fossil,t73f.de:fossil"}
var EnvDirectProxy = []string{"GOPROXY=direct"}
var 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...)
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
136
137
138
139
140
141
142











143
144
145
146
147
148
149







-
-
-
-
-
-
-
-
-
-
-







}

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 {

Changes to usecase/create_zettel.go.

79
80
81
82
83
84
85
86
87


88
89
90
91

92
93
94

95
96
97
98
99
100
101
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 {
// 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, "Sequel", "Sequel of "))
		m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeyPrequel, origMeta.Zid.String())
	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

Added 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 web/adapter/api/api.go.

92
93
94
95
96
97
98



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







+
+
+







	}
	if pol.CanRead(user, m) {
		result |= api.ZettelCanRead
	}
	if pol.CanWrite(user, m, m) {
		result |= api.ZettelCanWrite
	}
	if pol.CanRename(user, m) {
		result |= api.ZettelCanRename
	}
	if pol.CanDelete(user, m) {
		result |= api.ZettelCanDelete
	}
	if result == 0 {
		return api.ZettelCanNone
	}
	return result

Changes to web/adapter/api/command.go.

21
22
23
24
25
26
27
28
29


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

46
47
48
49
50
51
52
21
22
23
24
25
26
27


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

45
46
47
48
49
50
51
52







-
-
+
+















-
+







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

Changes to web/adapter/api/create_zettel.go.

23
24
25
26
27
28
29
30
31


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


30
31
32
33
34
35
36
37
38







-
-
+
+







	"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) {
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)
70
71
72
73
74
75
76
77
78


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

Changes to web/adapter/api/get_data.go.

18
19
20
21
22
23
24
25
26


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

39
18
19
20
21
22
23
24


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

38
39







-
-
+
+











-
+


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

30
31
32
33
34
35
36
37

38
39
40
41
42

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

55
56
57

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80




81
82
83
84
85
86
87
30
31
32
33
34
35
36

37





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

50
51
52

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




73
74
75
76
77
78
79
80
81
82
83







-
+
-
-
-
-
-
+











-
+


-
+



















-
-
-
-
+
+
+
+







	"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(
func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc {
	getZettel usecase.GetZettel,
	parseZettel usecase.ParseZettel,
	evaluate usecase.Evaluate,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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(ctx, w, zid, part, getZettel)
			a.writePlainData(w, ctx, zid, part, getZettel)

		case api.EncoderData:
			a.writeSzData(ctx, w, zid, part, getZettel)
			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(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	}
}

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)
113
114
115
116
117
118
119
120

121
122
123
124
125
126
127
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(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
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 {

Changes to web/adapter/api/login.go.

21
22
23
24
25
26
27
28
29


30
31
32
33
34
35
36
21
22
23
24
25
26
27


28
29
30
31
32
33
34
35
36







-
-
+
+







	"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) {
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
47
48
49
50
51
52
53
54

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


70
71
72
73
74
75
76
47
48
49
50
51
52
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) {
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
		}
94
95
96
97
98
99
100
101

102
103
104
105
106
107
108
109
110
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.

30
31
32
33
34
35
36
37

38
39
40
41
42
43

44
45
46
47
48
49
50
30
31
32
33
34
35
36

37






38
39
40
41
42
43
44
45







-
+
-
-
-
-
-
-
+







	"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(
func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc {
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,
	reIndex *usecase.ReIndex,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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)
97
98
99
100
101
102
103
104
105


106
107

108
109
110
111
112
113

114
115
116
117
118
119

120
121
122
123
124
125
126
127
128
129
130
131

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

149
150
151
152

153
154
155
156
157
158
159
92
93
94
95
96
97
98


99
100
101

102
103
104
105
106
107

108
109
110
111
112
113

114
115
116
117
118
119
120
121
122
123
124
125

126
127
128
129
130
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 {
	minVal, maxVal := -1, -1
	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 {
					minVal = num
					min = num
					continue
				}
			}
			if strings.HasPrefix(act, api.MaxAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					maxVal = num
					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, minVal, maxVal)
				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, minVal, maxVal int) error {
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) < minVal || (maxVal > 0 && len(ml0) > maxVal) {
		if len(ml0) < min || (max > 0 && len(ml0) > max) {
			continue
		}
		arr[k0] = ml0
	}
	return enc.writeArrangement(w, key, arr)
}

Added web/adapter/api/rename_zettel.go.







































































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

package api

import (
	"net/http"
	"net/url"

	"t73f.de/r/zsc/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/update_zettel.go.

20
21
22
23
24
25
26
27
28


29
30
31
32
33
34
35
20
21
22
23
24
25
26


27
28
29
30
31
32
33
34
35







-
-
+
+







	"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) {
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()
49
50
51
52
53
54
55
56
57


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

66
67
68
69
70
71
72




73
74
75
76
77
78
79
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

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
15
16
17
18
19
20
21
22
23
24
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"
	valueActionSequel  = "sequel"
	valueActionVersion = "version"
)

// Enumeration for queryKeyAction
type createAction uint8

const (
	actionCopy createAction = iota
	actionFolge
	actionNew
	actionSequel
	actionChild createAction = iota
	actionCopy
	actionFolge
	actionNew
	actionVersion
)

var createActionMap = map[string]createAction{
	valueActionSequel:  actionSequel,
	valueActionChild:   actionChild,
	valueActionCopy:    actionCopy,
	valueActionFolge:   actionFolge,
	valueActionNew:     actionNew,
	valueActionVersion: actionVersion,
}

func getCreateAction(s string) createAction {

Changes to web/adapter/webui/create_zettel.go.

32
33
34
35
36
37
38
39

40
41

42
43
44

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


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

76
77
78
79
80
81
82
32
33
34
35
36
37
38

39


40



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68


69
70
71

72
73
74
75
76
77
78
79







-
+
-
-
+
-
-
-
+

















+
+








-
-



-
+







	"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,
	getZettel usecase.GetZettel, createZettel *usecase.CreateZettel,
	createZettel *usecase.CreateZettel,
	ucListRoles usecase.ListRoles,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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 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
}
125
126
127
128
129
130
131
132
133


134
135
136
137
138
139
140
122
123
124
125
126
127
128


129
130
131
132
133
134
135
136
137







-
-
+
+







	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) {
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 {
150
151
152
153
154
155
156
157

158
159
160
161
162
163

164
165
166


167
168

169
170
171
172
173
174
175
147
148
149
150
151
152
153

154
155
156
157
158
159

160



161
162


163
164
165
166
167
168
169
170







-
+





-
+
-
-
-
+
+
-
-
+







			return
		}
		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,
	queryMeta *usecase.Query, evaluate *usecase.Evaluate,
	evaluate *usecase.Evaluate,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {

) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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
		}
188
189
190
191
192
193
194
195
196


183
184
185
186
187
188
189


190
191







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

25
26
27
28
29
30
31
32

33
34
35
36

37
38
39
40
41
42
43
25
26
27
28
29
30
31

32




33
34
35
36
37
38
39
40







-
+
-
-
-
-
+







	"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(
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc {
	getZettel usecase.GetZettel,
	getAllZettel usecase.GetAllZettel,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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
		}
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77







-
+







			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
99
100
101
102
103
104
105
106
107


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


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.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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('/'))
	})
}
	}
}

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
59
60
61
62
20
21
22
23
24
25
26

27





28
29
30
31
32
33
34
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(
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	getZettel usecase.GetZettel,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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
		}
78
79
80
81
82
83
84
85
86


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

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
48


18
19
20
21
22
23
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"
)

// 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) {
func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			wui.log.Debug().Err(err).Msg("Favicon not found")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		defer f.Close()

		data, err := io.ReadAll(f)
		if err != nil {
			wui.log.Error().Err(err).Msg("Unable to read favicon data")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}

		if err = adapter.WriteData(w, data, ""); err != nil {
			wui.log.Error().Err(err).Msg("Write favicon")
		}
	})
}
	}
}

Changes to web/adapter/webui/get_info.go.

37
38
39
40
41
42
43
44
45


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


44
45
46
47
48
49
50
51
52







-
-
+
+







// 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) {
) 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})
113
114
115
116
117
118
119
120

121
122
123
124
125
126
127
113
114
115
116
117
118
119

120
121
122
123
124
125
126
127







-
+







			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

Changes to web/adapter/webui/get_zettel.go.

27
28
29
30
31
32
33
34

35
36
37
38

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

34




35
36
37
38
39
40
41
42







-
+
-
-
-
-
+







	"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(
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc {
	evaluate *usecase.Evaluate,
	getZettel usecase.GetZettel,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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
		}
71
72
73
74
75
76
77
78

79
80
81
82
83

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

101
102
103
104
105
106
107
68
69
70
71
72
73
74

75
76
77
78
79

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

97
98
99
100
101
102
103
104







-
+




-
+
















-
+







		}
		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("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, "sequel", zn.InhMeta, api.KeySequel, config.KeyShowSequelLinks, 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

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
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) {
func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		// Currently, command "refresh" is the only command to be executed.
		err := ucRefresh.Run(ctx)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	})
	}
}

Changes to web/adapter/webui/home.go.

22
23
24
25
26
27
28
29

30
31
32
33
34
35


36
37
38
39
40
41
42
22
23
24
25
26
27
28

29
30
31
32
33


34
35
36
37
38
39
40
41
42







-
+




-
-
+
+







	"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 {
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 getRootPort) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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()
53
54
55
56
57
58
59
60
61


53
54
55
56
57
58
59


60
61







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

127
128
129
130
131
132
133
134

135
136
137
138
139
140
141




142
143
144
145
146
147
148
127
128
129
130
131
132
133

134
135
136
137




138
139
140
141
142
143
144
145
146
147
148







-
+



-
-
-
-
+
+
+
+







			return obj
		}
		u := builder.NewURLBuilder('h').AppendQuery(q)
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object {
		attr, _, rest := findA(obj)
		attr, assoc, 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)
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrClass, sx.MakeString("external"))).
			Cons(sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank"))).
			Cons(sx.Cons(shtml.SymAttrRel, sx.MakeString("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())
271
272
273
274
275
276
277
278

279
280
281
282
283
284
285
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, shtml.Endnotes(&env), nil
	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
	}

Changes to web/adapter/webui/lists.go.

36
37
38
39
40
41
42
43

44
45
46
47
48

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

43





44
45
46
47
48
49
50
51







-
+
-
-
-
-
-
+







	"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(
func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc {
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,
	reIndex *usecase.ReIndex) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	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()
150
151
152
153
154
155
156
157

158
159
160
161
162
163
164
146
147
148
149
150
151
152

153
154
155
156
157
158
159
160







-
+







			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 {

Changes to web/adapter/webui/login.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
50
51
52
53
54
55


56
57
58
59
60
61
62
23
24
25
26
27
28
29


30
31
32
33
34
35
36
37
38

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

// 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) {
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.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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 {
71
72
73
74
75
76
77
78
79


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

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

	"t73f.de/r/zsc/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/template.go.

136
137
138
139
140
141
142
143

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

143
144
145
146
147
148
149
150







-
+







		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")),
					sx.Cons(shtml.SymAttrRel, sx.MakeString("noopener noreferrer")),
				),
				text)
		}
	}
	return text
}

249
250
251
252
253
254
255
256

257
258



259
260
261
262
263
264
265
249
250
251
252
253
254
255

256
257
258
259
260
261
262
263
264
265
266
267
268







-
+


+
+
+







	}
	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("child-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String()))
		rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canRename(ctx, user, m) {
		rb.bindString("rename-url", sx.MakeString(newURLBuilder('b').SetZid(apiZid).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

Changes to web/adapter/webui/webui.go.

78
79
80
81
82
83
84

85
86
87
88
89
90
91
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')
170
171
172
173
174
175
176




177
178
179
180
181
182
183
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188







+
+
+
+







}

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)

Changes to web/content/content.go.

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

27
28
29
30
31
32
33







-







	"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"
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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",
}

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

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

53
54
55
56
57




58
59
60
61
62
63
64
65
66
67
68


69
70
71
72
73
74
75
31
32
33
34
35
36
37













38

39
40




41
42
43
44
45










46
47
48
49
50
51
52
53
54







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

-
+

-
-
-
-
+
+
+
+

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







	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 {
func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server {
	srv := myServer{
		log:              sd.Log,
		baseURL:          sd.BaseURL,
		persistentCookie: sd.PersistentCookie,
		secureCookie:     sd.SecureCookie,
		log:              log,
		baseURL:          baseURL,
		persistentCookie: persistentCookie,
		secureCookie:     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)
	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) {

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
75

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
12
13
14
15
16
17
18

19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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"
	"net/http/pprof"
	"regexp"
	rtprf "runtime/pprof"
	"strings"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
)

type (
	methodHandler [server.MethodLAST]http.Handler
	routingTable  [256]*methodHandler
)

var mapMethod = map[string]server.Method{
	http.MethodHead:   server.MethodHead,
	http.MethodGet:    server.MethodGet,
	http.MethodPost:   server.MethodPost,
	http.MethodPut:    server.MethodPut,
	http.MethodDelete: server.MethodDelete,
	api.MethodMove:    server.MethodMove,
}

// httpRouter handles all routing for zettelstore.
type httpRouter struct {
	log         *logger.Logger
	urlPrefix   string
	auth        auth.TokenManager
	minKey      byte
	maxKey      byte
	reURL       *regexp.Regexp
	listTable   routingTable
	zettelTable routingTable
	ur          server.UserRetriever
	mux         *http.ServeMux
	maxReqSize  int64
}

type routerData struct {
	log            *logger.Logger
	urlPrefix      string
	maxRequestSize int64
	auth           auth.TokenManager
	profiling      bool
}

// initializeRouter creates a new, empty router with the given root handler.
func (rt *httpRouter) initializeRouter(rd routerData) {
	rt.log = rd.log
	rt.urlPrefix = rd.urlPrefix
	rt.auth = rd.auth
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 = rd.maxRequestSize
	rt.maxReqSize = 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.

34
35
36
37
38
39
40

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







+








// Values for method type
const (
	MethodGet Method = iota
	MethodHead
	MethodPost
	MethodPut
	MethodMove
	MethodDelete
	MethodLAST // must always be the last one
)

// Router allows to state routes for various URL paths.
type Router interface {
	Handle(pattern string, handler http.Handler)

Changes to www/build.md.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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),
* [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
29
30
31
32
33
34
35

36


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

52


53
54
55

56


57
58
59
60

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87



88
89
90
91
92
93
26
27
28
29
30
31
32
33

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

52
53
54
55
56
57

58
59
60
61
62

63
64
65
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81
82
83
84
85
86



87
88
89
90
91
92
93
94
95







+
-
+
+















+
-
+
+



+
-
+
+



-
+










-













-
-
-
+
+
+







## 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
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
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
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/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://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.
special platforms, you allow ot to download a Fossil repository from the host
`zettelstore.de`. The build tool set `GOVCS` to the right value, but you may
use other `go` commands that try to download a Fossil repository.

On some operating systems, namely Termux on Android, an error message might
state that an user cannot be determined (`cannot determine user`). In this
case, Fossil is allowed to download the repository, but cannot associate it
with an user name. Set the environment variable `USER` to any user name, like:
`USER=nobody go run tools/build.go build`.

Changes to www/changes.wiki.

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































5
6
7
8
9
10
11




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







<title>Change Log</title>

<a id="0_19"></a>
<h2>Changes for Version 0.19.0 (pending)</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 zettel with ID starting with 0000 is not indexed any
     more. If you search / query for zettel content, these zettel will not be
     returned.
     (breaking: api, webui)
  *  Metadata keys <code>superior</code> and <code>subordinate</code> are not
     supported from 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)
  *  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)

<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

Changes to www/impri.wiki.

10
11
12
13
14
15
16
17

18
10
11
12
13
14
15
16

17
18







-
+

If you do not log into this site, or login as the user &quot;anonymous&quot;,
the only personal data this web service will process is your IP adress. It will
be used to send the data of the website you requested to you and to mitigate
possible attacks against this website.

This website is hosted by [https://ionos.de|1&amp;1 IONOS SE].
According to
[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-ionos-produktes/andere-ionos-produkte/|their information],
[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.

Changes to www/index.wiki.

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

41
42
43
44
45
46







-






  *  [/timeline?df=v0.18.0&y=ci|Check-ins derived from the 0.18.0 release],
     [/vdiff?from=v0.18.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.

73
74
75
76
77
78
79
80

81
82
83
84
85
86
87
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() {
		if !input.IsSpace(inp.Ch) {
			break
		}
		inp.Next()
	}
	zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace)
}

Changes to zettel/id/id.go.

42
43
44
45
46
47
48

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







+







	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)
160
161
162
163
164
165
166



























































































161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
	}
	res, err := Parse(s)
	if err != nil {
		panic(err)
	}
	return res
}

// ----- Base36 zettel identifier.

// ZidN is the internal identifier of a zettel. It is a number in the range
// 1..36^4-1 (1..1679615), as it is externally represented by four alphanumeric
// characters.
type ZidN uint32

// Some important ZettelIDs.
const (
	InvalidN = ZidN(0) // Invalid is a Zid that will never be valid
)

const maxZidN = 36*36*36*36 - 1

// ParseUintN interprets a string as a possible zettel identifier
// and returns its integer value.
func ParseUintN(s string) (uint64, error) {
	res, err := strconv.ParseUint(s, 36, 21)
	if err != nil {
		return 0, err
	}
	if res == 0 || res > maxZidN {
		return res, strconv.ErrRange
	}
	return res, nil
}

// ParseN interprets a string as a zettel identification and
// returns its value.
func ParseN(s string) (ZidN, error) {
	if len(s) != 4 {
		return InvalidN, strconv.ErrSyntax
	}
	res, err := ParseUintN(s)
	if err != nil {
		return InvalidN, err
	}
	return ZidN(res), nil
}

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

// String converts the zettel identification to a string of 14 digits.
// Only defined for valid ids.
func (zid ZidN) String() string {
	var result [4]byte
	zid.toByteArray(&result)
	return string(result[:])
}

// ZettelID return the zettel identification as a api.ZettelID.
func (zid ZidN) ZettelID() api.ZettelID { return api.ZettelID(zid.String()) }

// Bytes converts the zettel identification to a byte slice of 14 digits.
// Only defined for valid ids.
func (zid ZidN) Bytes() []byte {
	var result [4]byte
	zid.toByteArray(&result)
	return result[:]
}

// toByteArray converts the Zid into a fixed byte array, usable for printing.
//
// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly"
// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/
func (zid ZidN) toByteArray(result *[4]byte) {
	d12 := uint32(zid) / (36 * 36)
	d1 := d12 / 36
	d2 := d12 % 36
	d34 := uint32(zid) % (36 * 36)
	d3 := d34 / 36
	d4 := d34 % 36

	const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
	result[0] = digits[d1]
	result[1] = digits[d2]
	result[2] = digits[d3]
	result[3] = digits[d4]
}

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

Changes to zettel/id/id_test.go.

11
12
13
14
15
16
17

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







+







// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package id_test provides unit tests for testing zettel id specific functions.
package id_test

import (
	"strings"
	"testing"

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

func TestIsValid(t *testing.T) {
	t.Parallel()
86
87
88
89
90
91
92
































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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
func BenchmarkBytes(b *testing.B) {
	var bs []byte
	for range b.N {
		bs = id.Zid(12345678901200).Bytes()
	}
	bResult = bs
}

// ----- Base-36 identifier

func TestIsValidN(t *testing.T) {
	t.Parallel()
	validIDs := []string{
		"0001", "0020", "0300", "4000",
		"zzzz", "ZZZZ", "Cafe", "bAbE",
	}

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

	invalidIDs := []string{
		"", "0", "a", "de", "dfg", "abcde",
		"012.",
		"+1234", "+123",
	}

	for i, sid := range invalidIDs {
		if zid, err := id.ParseN(sid); err == nil {
			t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid)
		}
	}
}

Deleted zettel/id/mapper/mapper.go.

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












































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package mapper provides a mechanism to map zettel identifier with 14 digits
// to identifier with four characters.
package mapper

import (
	"bytes"
	"context"
	"fmt"
	"maps"
	"strconv"
	"sync"
	"time"

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

// ----- Base36 zettel identifier.

// ZidN is the internal identifier of a zettel. It is a number in the range
// 1..36^4-1 (1..1679615), as it is externally represented by four alphanumeric
// characters.
type ZidN uint32

// Some important ZettelIDs.
const (
	InvalidN = ZidN(0) // Invalid is a Zid that will never be valid
)

const maxZidN = 36*36*36*36 - 1

// ParseUintN interprets a string as a possible zettel identifier
// and returns its integer value.
func ParseUintN(s string) (uint64, error) {
	res, err := strconv.ParseUint(s, 36, 21)
	if err != nil {
		return 0, err
	}
	if res == 0 || res > maxZidN {
		return res, strconv.ErrRange
	}
	return res, nil
}

// ParseN interprets a string as a zettel identification and
// returns its value.
func ParseN(s string) (ZidN, error) {
	if len(s) != 4 {
		return InvalidN, strconv.ErrSyntax
	}
	res, err := ParseUintN(s)
	if err != nil {
		return InvalidN, err
	}
	return ZidN(res), nil
}

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

// String converts the zettel identification to a string of 14 digits.
// Only defined for valid ids.
func (zid ZidN) String() string {
	var result [4]byte
	zid.toByteArray(&result)
	return string(result[:])
}

// ZettelID return the zettel identification as a api.ZettelID.
func (zid ZidN) ZettelID() api.ZettelID { return api.ZettelID(zid.String()) }

// Bytes converts the zettel identification to a byte slice of 14 digits.
// Only defined for valid ids.
func (zid ZidN) Bytes() []byte {
	var result [4]byte
	zid.toByteArray(&result)
	return result[:]
}

// toByteArray converts the Zid into a fixed byte array, usable for printing.
//
// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly"
// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/
func (zid ZidN) toByteArray(result *[4]byte) {
	d12 := uint32(zid) / (36 * 36)
	d1 := d12 / 36
	d2 := d12 % 36
	d34 := uint32(zid) % (36 * 36)
	d3 := d34 / 36
	d4 := d34 % 36

	const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	result[0] = digits[d1]
	result[1] = digits[d2]
	result[2] = digits[d3]
	result[3] = digits[d4]
}

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

// Mapper transforms old-style zettel identifier (14 digits) into new one (4 alphanums).
//
// Since there are no new-style identifier defined, there is only support for old-style
// identifier by checking, whether they are suported as new-style or not.
//
// This will change in later versions.
type Mapper struct {
	fetcher   Fetcher
	defined   map[id.Zid]ZidN // predefined mapping, constant after creation
	mx        sync.RWMutex    // protect toNew ... nextZidN
	toNew     map[id.Zid]ZidN // working mapping old->new
	toOld     map[ZidN]id.Zid // working mapping new->old
	nextZidM  ZidN            // next zid for manual
	hadManual bool
	nextZidN  ZidN // next zid for normal zettel
}

// Fetcher is an object that will fetch all identifier currently in user.
type Fetcher interface {
	// FetchZidsO fetch all old-style zettel identifier.
	FetchZidsO(context.Context) (*id.Set, error)
}

// Make creates a new Mapper.
func Make(fetcher Fetcher) *Mapper {
	defined := map[id.Zid]ZidN{
		id.Invalid: InvalidN,
		1:          MustParseN("0001"), // ZidVersion
		2:          MustParseN("0002"), // ZidHost
		3:          MustParseN("0003"), // ZidOperatingSystem
		4:          MustParseN("0004"), // ZidLicense
		5:          MustParseN("0005"), // ZidAuthors
		6:          MustParseN("0006"), // ZidDependencies
		7:          MustParseN("0007"), // ZidLog
		8:          MustParseN("0008"), // ZidMemory
		9:          MustParseN("0009"), // ZidSx
		10:         MustParseN("000a"), // ZidHTTP
		11:         MustParseN("000b"), // ZidAPI
		12:         MustParseN("000c"), // ZidWebUI
		13:         MustParseN("000d"), // ZidConsole
		20:         MustParseN("000e"), // ZidBoxManager
		21:         MustParseN("000f"), // ZidZettel
		22:         MustParseN("000g"), // ZidIndex
		23:         MustParseN("000h"), // ZidQuery
		90:         MustParseN("000i"), // ZidMetadataKey
		92:         MustParseN("000j"), // ZidParser
		96:         MustParseN("000k"), // ZidStartupConfiguration
		100:        MustParseN("000l"), // ZidRuntimeConfiguration
		101:        MustParseN("000m"), // ZidDirectory
		102:        MustParseN("000n"), // ZidWarnings
		10100:      MustParseN("000s"), // Base HTML Template
		10200:      MustParseN("000t"), // Login Form Template
		10300:      MustParseN("000u"), // List Zettel Template
		10401:      MustParseN("000v"), // Detail Template
		10402:      MustParseN("000w"), // Info Template
		10403:      MustParseN("000x"), // Form Template
		10405:      MustParseN("000y"), // Delete Template
		10700:      MustParseN("000z"), // Error Template
		19000:      MustParseN("000q"), // Sxn Start Code
		19990:      MustParseN("000r"), // Sxn Base Code
		20001:      MustParseN("0010"), // Base CSS
		25001:      MustParseN("0011"), // User CSS
		40001:      MustParseN("000o"), // Generic Emoji
		59900:      MustParseN("000p"), // Sxn Prelude
		60010:      MustParseN("0012"), // zettel
		60020:      MustParseN("0013"), // confguration
		60030:      MustParseN("0014"), // role
		60040:      MustParseN("0015"), // tag
		90000:      MustParseN("0016"), // New Menu
		90001:      MustParseN("0017"), // New Zettel
		90002:      MustParseN("0018"), // New User
		90003:      MustParseN("0019"), // New Tag
		90004:      MustParseN("001a"), // New Role
		// 100000000,   // Manual               -> 0020-00yz
		9999999997:  MustParseN("00zx"), // ZidSession
		9999999998:  MustParseN("00zy"), // ZidAppDirectory
		9999999999:  MustParseN("00zz"), // ZidMapping
		10000000000: MustParseN("0100"), // ZidDefaultHome
	}
	toNew := maps.Clone(defined)
	toOld := make(map[ZidN]id.Zid, len(toNew))
	for o, n := range toNew {
		if _, found := toOld[n]; found {
			panic("duplicate predefined zid")
		}
		toOld[n] = o
	}

	return &Mapper{
		fetcher:   fetcher,
		defined:   defined,
		toNew:     toNew,
		toOld:     toOld,
		nextZidM:  MustParseN("0020"),
		hadManual: false,
		nextZidN:  MustParseN("0101"),
	}
}

// isWellDefined returns true, if the given zettel identifier is predefined
// (as stated in the manual), or is part of the manual itself, or is greater than
// 19699999999999.
func (zm *Mapper) isWellDefined(zid id.Zid) bool {
	if _, found := zm.defined[zid]; found || (1000000000 <= zid && zid <= 1099999999) {
		return true
	}
	if _, err := time.Parse("20060102150405", zid.String()); err != nil {
		return false
	}
	return 19700000000000 <= zid
}

// Warnings returns all zettel identifier with warnings.
func (zm *Mapper) Warnings(ctx context.Context) (*id.Set, error) {
	allZidsO, err := zm.fetcher.FetchZidsO(ctx)
	if err != nil {
		return nil, err
	}
	warnings := id.NewSet()
	allZidsO.ForEach(func(zid id.Zid) {
		if !zm.isWellDefined(zid) {
			warnings = warnings.Add(zid)
		}
	})
	return warnings, nil
}

// LookupZidN returns the new-style identifier for a given old-style identifier.
func (zm *Mapper) LookupZidN(zidO id.Zid) (ZidN, bool) {
	if !zidO.IsValid() {
		panic(zidO)
	}
	zm.mx.RLock()
	zidN, found := zm.toNew[zidO]
	zm.mx.RUnlock()
	return zidN, found
}

// AllocateZidN allocates a new new-style identifier, which is associated with
// the given old-style identifier.
func (zm *Mapper) AllocateZidN(zidO id.Zid) ZidN {
	if zidN, found := zm.LookupZidN(zidO); found {
		return zidN
	}

	zm.mx.Lock()
	defer zm.mx.Unlock()
	// Double check to avoid races
	if zidN, found := zm.toNew[zidO]; found {
		return zidN
	}

	if 1000000000 <= zidO && zidO <= 1099999999 {
		if zidO == 1000000000 {
			zm.hadManual = true
		}
		if zm.hadManual {
			zidN := zm.nextZidM
			zm.nextZidM++
			zm.toNew[zidO] = zidN
			zm.toOld[zidN] = zidO
			return zidN
		}
	}

	zidN := zm.nextZidN
	zm.nextZidN++
	zm.toNew[zidO] = zidN
	zm.toOld[zidN] = zidO
	return zidN
}

// LookupZidO returns the old-style identifier for a new-style identifier.
func (zm *Mapper) LookupZidO(zidN ZidN) (id.Zid, bool) {
	if zm != nil {
		zm.mx.RLock()
		zidO, found := zm.toOld[zidN]
		zm.mx.RUnlock()
		return zidO, found
	}
	return id.Invalid, false
}

// DeleteO removes a mapping with the given old-style identifier.
func (zm *Mapper) DeleteO(zidO id.Zid) {
	if _, found := zm.defined[zidO]; found {
		return
	}
	zm.mx.Lock()
	if zidN, found := zm.toNew[zidO]; found {
		delete(zm.toNew, zidO)
		delete(zm.toOld, zidN)
		if lastZidN := zm.nextZidN - 1; zidN == lastZidN {
			zm.nextZidN = lastZidN
		}
	}
	zm.mx.Unlock()
}

// AsBytes returns the current mapping as lines, where each line contains the
// old and the new zettel identifier.
func (zm *Mapper) AsBytes() []byte {
	zm.mx.RLock()
	defer zm.mx.RUnlock()

	allZidsO := id.NewSetCap(len(zm.toNew))
	for zidO := range zm.toNew {
		allZidsO = allZidsO.Add(zidO)
	}
	var buf bytes.Buffer
	first := true
	allZidsO.ForEach(func(zidO id.Zid) {
		if !first {
			buf.WriteByte('\n')
		}
		first = false
		zidN := zm.toNew[zidO]
		buf.WriteString(zidO.String())
		buf.WriteByte(' ')
		buf.WriteString(zidN.String())
	})
	return buf.Bytes()
}

// Fetch all zettel and update the mapping.
func (zm *Mapper) Fetch(ctx context.Context) error {
	allZidsO, err := zm.fetcher.FetchZidsO(ctx)
	if err != nil {
		return err
	}
	allZidsO.ForEach(func(zidO id.Zid) {
		zm.AllocateZidN(zidO)
	})
	zm.mx.Lock()
	defer zm.mx.Unlock()
	if len(zm.toNew) != allZidsO.Length() {
		for zidO, zidN := range zm.toNew {
			if allZidsO.Contains(zidO) {
				continue
			}
			delete(zm.toNew, zidO)
			delete(zm.toOld, zidN)
		}
	}
	return nil
}

// ParseAndUpdate parses the given content and updates the Mapping.
func (zm *Mapper) ParseAndUpdate(content []byte) (err error) {
	zm.mx.Lock()
	defer zm.mx.Unlock()
	inp := input.NewInput(content)
	for inp.Ch != input.EOS {
		inp.SkipSpace()
		pos := inp.Pos
		zidO := readZidO(inp)
		if !zidO.IsValid() {
			inp.SkipToEOL()
			inp.EatEOL()
			if err == nil {
				err = fmt.Errorf("unable to parse old zid: %q", string(inp.Src[pos:inp.Pos]))
			}
			continue
		}
		inp.SkipSpace()
		zidN := readZidN(inp)
		if !zidN.IsValid() {
			inp.SkipToEOL()
			inp.EatEOL()
			if err == nil {
				err = fmt.Errorf("unable to parse new zid: %q", string(inp.Src[pos:inp.Pos]))
			}
			continue
		}
		inp.SkipToEOL()
		inp.EatEOL()

		if oldZidN, found := zm.toNew[zidO]; found {
			if oldZidN != zidN {
				err = fmt.Errorf("old zid %v already mapped to %v, overwrite: %v", zidO, oldZidN, zidN)
			}
			continue
		}
		zm.toNew[zidO] = zidN
		zm.toOld[zidN] = zidO
		zm.nextZidN = max(zm.nextZidN, zidN+1)
	}
	return err
}

func readZidO(inp *input.Input) id.Zid {
	pos := inp.Pos
	for '0' <= inp.Ch && inp.Ch <= '9' {
		inp.Next()
	}
	zidO, _ := id.Parse(string(inp.Src[pos:inp.Pos]))
	return zidO
}
func readZidN(inp *input.Input) ZidN {
	pos := inp.Pos
	for ('0' <= inp.Ch && inp.Ch <= '9') || ('a' <= inp.Ch && inp.Ch <= 'z') || ('A' <= inp.Ch && inp.Ch <= 'Z') {
		inp.Next()
	}
	zidN, _ := ParseN(string(inp.Src[pos:inp.Pos]))
	return zidN
}

Changes to zettel/id/set.go.

283
284
285
286
287
288
289
290
291



292
293
294
295
296
297
298
283
284
285
286
287
288
289


290
291
292
293
294
295
296
297
298
299







-
-
+
+
+







}

// ----- unchecked base operations

func newFromSlice(seq Slice) *Set {
	if l := len(seq); l == 0 {
		return nil
	}
	return &Set{seq: seq}
	} else {
		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)
	}
}

Changes to zettel/meta/meta.go.

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

134
135
136
137
138
139
140







-







	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, "")
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
148
149
150
151
152
153
154

155
156
157
158
159
160
161







-







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

Changes to zettel/meta/parse.go.

27
28
29
30
31
32
33
34

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

34
35
36
37
38
39
40
41







-
+







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()
		skipSpace(inp)
		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
60
61
62
63
64
65
66

67
68
69
70
71
72

73
74
75
76
77

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







-
+





-
+




-
+






+
+
+
+
+
+








func parseHeader(m *Meta, inp *input.Input) {
	pos := inp.Pos
	for isHeader(inp.Ch) {
		inp.Next()
	}
	key := inp.Src[pos:inp.Pos]
	inp.SkipSpace()
	skipSpace(inp)
	if inp.Ch == ':' {
		inp.Next()
	}
	var val []byte
	for {
		inp.SkipSpace()
		skipSpace(inp)
		pos = inp.Pos
		skipToEOL(inp)
		val = append(val, inp.Src[pos:inp.Pos]...)
		inp.EatEOL()
		if !inp.IsSpace() {
		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
		}