Zettelstore

Check-in Differences
Login

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

Difference From v0.13.0 To trunk

2023-12-01
13:29
Cleanup changes file: tt tag to code tag ... (Leaf check-in: 93010b5f17 user: stern tags: trunk)
13:02
Fix some omissions in the manual ... (check-in: 9804546d28 user: stern tags: trunk)
2023-08-07
13:56
Typo on home page ... (check-in: d2fe74163e user: stern tags: trunk)
13:53
Version 0.13.0 ... (check-in: 37fed58a18 user: stern tags: trunk, release, v0.13.0)
10:49
Update changelog ... (check-in: 8da4351a55 user: stern tags: trunk)

Changes to VERSION.

1
0.13.0
|
1
0.17.0-dev

Changes to ast/inline.go.

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

205
206
207
208
209
210
211
212

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

// Constants for FormatCode
const (
	_            FormatKind = iota
	FormatEmph              // Emphasized text.
	FormatStrong            // Strongly emphasized text.
	FormatInsert            // Inserted text.
	FormatDelete            // Deleted text.
	FormatSuper             // Superscripted text.
	FormatSub               // SubscriptedText.
	FormatQuote             // Quoted text.

	FormatSpan              // Generic inline container.
)

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

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








|
|
|
|
|
|
|
>
|







191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213

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

// Constants for FormatCode
const (
	_            FormatKind = iota
	FormatEmph              // Emphasized text
	FormatStrong            // Strongly emphasized text
	FormatInsert            // Inserted text
	FormatDelete            // Deleted text
	FormatSuper             // Superscripted text
	FormatSub               // SubscriptedText
	FormatQuote             // Quoted text
	FormatMark              // Marked text
	FormatSpan              // Generic inline container
)

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

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

Changes to auth/cred/cred.go.

40
41
42
43
44
45
46
47
48
49
50
51
52
53
		return false, nil
	}
	return false, err
}

func createFullCredential(zid id.Zid, ident, credential string) []byte {
	var buf bytes.Buffer
	buf.WriteString(zid.String())
	buf.WriteByte(' ')
	buf.WriteString(ident)
	buf.WriteByte(' ')
	buf.WriteString(credential)
	return buf.Bytes()
}







|






40
41
42
43
44
45
46
47
48
49
50
51
52
53
		return false, nil
	}
	return false, err
}

func createFullCredential(zid id.Zid, ident, credential string) []byte {
	var buf bytes.Buffer
	buf.Write(zid.Bytes())
	buf.WriteByte(' ')
	buf.WriteString(ident)
	buf.WriteByte(' ')
	buf.WriteString(credential)
	return buf.Bytes()
}

Changes to auth/impl/impl.go.

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

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








|







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

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

Changes to auth/policy/box.go.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

// polBox implements a policy box.
type polBox struct {
	box    box.Box







|
<
<
<
<







20
21
22
23
24
25
26
27




28
29
30
31
32
33
34
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// BoxWithPolicy wraps the given box inside a policy box.
func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) {




	pol := newPolicy(manager, authConfig)
	return newBox(box, pol), pol
}

// polBox implements a policy box.
type polBox struct {
	box    box.Box
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
	return pp.box.CanUpdateZettel(ctx, zettel)
}

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







|







102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
	return pp.box.CanUpdateZettel(ctx, zettel)
}

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









func (pp *polBox) Refresh(ctx context.Context) error {
	user := server.GetUser(ctx)
	if pp.policy.CanRefresh(user) {
		return pp.box.Refresh(ctx)
	}
	return box.NewErrNotAllowed("Refresh", user, id.Invalid)
}
















>
>
>
>
>
>
>
>
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168

func (pp *polBox) Refresh(ctx context.Context) error {
	user := server.GetUser(ctx)
	if pp.policy.CanRefresh(user) {
		return pp.box.Refresh(ctx)
	}
	return box.NewErrNotAllowed("Refresh", user, id.Invalid)
}
func (pp *polBox) ReIndex(ctx context.Context, zid id.Zid) error {
	user := server.GetUser(ctx)
	if pp.policy.CanRefresh(user) {
		// If a user is allowed to refresh all data, it it also allowed to re-index a zettel.
		return pp.box.ReIndex(ctx, zid)
	}
	return box.NewErrNotAllowed("ReIndex", user, zid)
}

Changes to box/box.go.

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
















62
63
64
65
66
67
68

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

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

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

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

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

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

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

















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

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








<
<
<
<
<
<
<



<
<
<
<
<
<












>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







27
28
29
30
31
32
33







34
35
36






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

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








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







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

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

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

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

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

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

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

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

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

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

125
126
127
128
129
130
131

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



148
149
150
151
152
153
154
	// Refresh the box data.
	Refresh(context.Context)
}

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


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

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

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

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

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



}

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








>
















>
>
>







128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
	// Refresh the box data.
	Refresh(context.Context)
}

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

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

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

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

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

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

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

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

297
298
299
300
301
302
303
304

305

306
307
308
309
310
311
312
313
314
315
316
317

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

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

// ErrNotFound is returned if a zettel was not found in the box.

var ErrNotFound = errors.New("zettel not found")


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

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

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

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







|
>
|
>








|
|

|
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326

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

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

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

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

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

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

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

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

Changes to box/compbox/compbox.go.

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102
103
}

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

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

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

func (cb *compBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel")
	return id.Invalid, box.ErrReadOnly
}

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

	cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err")
	return zettel.Zettel{}, box.ErrNotFound
}

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








<
<
<
<
<
<
<















>
|
|







66
67
68
69
70
71
72







73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
}

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

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








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

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

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
				handle(m)
			}
		}
	}
	return nil
}

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

func (cb *compBox) UpdateZettel(context.Context, zettel.Zettel) error {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel")
	return box.ErrReadOnly
}

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

func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error {
	err := box.ErrNotFound
	if _, ok := myZettel[curZid]; ok {
		err = box.ErrReadOnly


	}
	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) error {
	err := box.ErrNotFound
	if _, ok := myZettel[zid]; ok {
		err = box.ErrReadOnly


	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (cb *compBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true







<
<
<
<
<
<
<





|
<


>
>







|
<


>
>







123
124
125
126
127
128
129







130
131
132
133
134
135

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

148
149
150
151
152
153
154
155
156
157
158
				handle(m)
			}
		}
	}
	return nil
}








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

func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) {

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

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

func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {

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

func (cb *compBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true

Changes to box/compbox/log.go.

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

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

func genLogC(*meta.Meta) []byte {
	const tsFormat = "2006-01-02 15:04:05.999999"
	entries := kernel.Main.RetrieveLogEntries()
	var buf bytes.Buffer







|







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

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

func genLogC(*meta.Meta) []byte {
	const tsFormat = "2006-01-02 15:04:05.999999"
	entries := kernel.Main.RetrieveLogEntries()
	var buf bytes.Buffer

Changes to box/constbox/base.css.

79
80
81
82
83
84
85

86
87
88
89
90
91
92
  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 }

  li,figure,figcaption,dl { margin: 0 }
  dt { margin: .5rem 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5rem 0 0 2rem }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: 0.5rem solid lightgray;







>







79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
  h1 { font-size:1.5rem;  margin:.65rem 0 }
  h2 { font-size:1.25rem; margin:.70rem 0 }
  h3 { font-size:1.15rem; margin:.75rem 0 }
  h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold }
  h5 { font-size:1.05rem; margin:.8rem 0 }
  h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter }
  p { margin: .5rem 0 0 0 }
  p.zs-meta-zettel { margin-top: .5rem; margin-left: 0.5rem }
  li,figure,figcaption,dl { margin: 0 }
  dt { margin: .5rem 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5rem 0 0 2rem }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: 0.5rem solid lightgray;

Changes to box/constbox/base.sxn.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`(@@@@
(html ,@(if lang `((@ (lang ,lang))))
(head
  (meta (@ (charset "utf-8")))
  (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta (@ (name "generator") (content "Zettelstore")))
  (meta (@ (name "format-detection") (content "telephone=no")))
  ,@META-HEADER
  (link (@ (rel "stylesheet") (href ,css-base-url)))
  (link (@ (rel "stylesheet") (href ,css-user-url)))
  ,@(if css-role-url `((link (@ (rel "stylesheet") (href ,css-role-url)))))
  (title ,title))
(body
  (nav (@ (class "zs-menu"))
    (a (@ (href ,home-url)) "Home")
    ,@(if with-auth
      `((div (@ (class "zs-dropdown"))
        (button "User")










|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`(@@@@
(html ,@(if lang `((@ (lang ,lang))))
(head
  (meta (@ (charset "utf-8")))
  (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta (@ (name "generator") (content "Zettelstore")))
  (meta (@ (name "format-detection") (content "telephone=no")))
  ,@META-HEADER
  (link (@ (rel "stylesheet") (href ,css-base-url)))
  (link (@ (rel "stylesheet") (href ,css-user-url)))
  ,@(ROLE-DEFAULT-meta (current-environment))
  (title ,title))
(body
  (nav (@ (class "zs-menu"))
    (a (@ (href ,home-url)) "Home")
    ,@(if with-auth
      `((div (@ (class "zs-dropdown"))
        (button "User")
36
37
38
39
40
41
42
43
44
45
46
47
48
      `((div (@ (class "zs-dropdown"))
        (button "New")
        (nav (@ (class "zs-dropdown-content"))
          ,@(map wui-link new-zettel-links)
       )))
    )
    (form (@ (action ,search-url))
      (input (@ (type "text") (placeholder "Search..") (name ,query-key-query))))
  )
  (main (@ (class "content")) ,DETAIL)
  ,@(if FOOTER `((footer (hr) ,@FOOTER)))
  ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!"))))
)))







|





36
37
38
39
40
41
42
43
44
45
46
47
48
      `((div (@ (class "zs-dropdown"))
        (button "New")
        (nav (@ (class "zs-dropdown-content"))
          ,@(map wui-link new-zettel-links)
       )))
    )
    (form (@ (action ,search-url))
      (input (@ (type "text") (placeholder "Search..") (name ,query-key-query) (dir "auto"))))
  )
  (main (@ (class "content")) ,DETAIL)
  ,@(if FOOTER `((footer (hr) ,@FOOTER)))
  ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!"))))
)))

Changes to box/constbox/constbox.go.

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

72
73
74
75
76
77
78
79
80
	number   int
	zettel   map[id.Zid]constZettel
	enricher box.Enricher
}

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

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

func (cb *constBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel")
	return id.Invalid, box.ErrReadOnly
}

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

	cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel")
	return zettel.Zettel{}, box.ErrNotFound
}

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








<
<
<
<
<
<
<





>
|
|







53
54
55
56
57
58
59







60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
	number   int
	zettel   map[id.Zid]constZettel
	enricher box.Enricher
}

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








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

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

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118


119
120
121
122
123
124
125
126
127
128
129


130
131
132
133
134
135
136
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

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

func (cb *constBox) UpdateZettel(context.Context, zettel.Zettel) error {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel")
	return box.ErrReadOnly
}

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) error {
	err := box.ErrNotFound
	if _, ok := cb.zettel[curZid]; ok {
		err = box.ErrReadOnly


	}
	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) error {
	err := box.ErrNotFound
	if _, ok := cb.zettel[zid]; ok {
		err = box.ErrReadOnly


	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (cb *constBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true







<
<
<
<
<
<
<





|
<


>
>







|
<


>
>







90
91
92
93
94
95
96







97
98
99
100
101
102

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

115
116
117
118
119
120
121
122
123
124
125
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}








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

func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) {

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

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

func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {

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

func (cb *constBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
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
	id.MustParse(api.ZidDependencies): {
		constHeader{
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20230601163100",
		},
		zettel.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155100",
			api.KeyModified:   "20230523144403",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.LoginTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20230527144100",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20230523212800",
			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:   "20230621131500",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.FormTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,







|










|



















|









|







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
	id.MustParse(api.ZidDependencies): {
		constHeader{
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20230601163100",
		},
		zettel.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155100",
			api.KeyModified:   "20230827212200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.LoginTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20230527144100",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20231126180500",
			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:   "20231023152000",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.FormTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
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
		zettel.NewContent(contentDeleteSxn)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230704122100",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ErrorTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Error HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20210305133215",
			api.KeyModified:   "20230527224800",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentErrorSxn)},










	id.TemplateSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Code for Templates",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",


			api.KeyVisibility: api.ValueVisibilityExpert,

		},











		zettel.NewContent(contentTemplateCodeSxn)},
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20200804111624",

			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20210622110143",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent([]byte("/* User-defined CSS */"))},
	id.RoleCSSMapZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Role to CSS Map",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxNone,
			api.KeyCreated:    "20220321183214",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(nil)},
	id.EmojiZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyCreated:    "20210504175807",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {
		constHeader{
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",

			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:      "New Zettel",
			api.KeyRole:       api.ValueRoleZettel,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20201028185209",


			api.KeyVisibility: api.ValueVisibilityCreator,






















		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewUser): {
		constHeader{
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.ValueRoleConfiguration,
			api.KeySyntax:                      meta.SyntaxNone,
			api.KeyCreated:                     "20201028185209",
			meta.NewPrefix + api.KeyCredential: "",
			meta.NewPrefix + api.KeyUserID:     "",
			meta.NewPrefix + api.KeyUserRole:   api.ValueUserRoleReader,
			api.KeyVisibility:                  api.ValueVisibilityOwner,
		},
		zettel.NewContent(nil)},








































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







>













>
>
>
>
>
>
>
>
>
>
|

|



>
>

>

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






>












<
<
<
<
<
<
<
<
<

















>





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














>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316









317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
		zettel.NewContent(contentDeleteSxn)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230704122100",
			api.KeyModified:   "20231129112800",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ErrorTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Error HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20210305133215",
			api.KeyModified:   "20230527224800",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentErrorSxn)},
	id.StartSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Start Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230824160700",
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnBase),
		},
		zettel.NewContent(contentStartCodeSxn)},
	id.BaseSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Base Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",
			api.KeyModified:   "20231012154500",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnPrelude),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.PreludeSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Prelude",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20231006181700",
			api.KeyModified:   "20231019140400",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentPreludeSxn)},
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20231129112800",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20210622110143",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent([]byte("/* User-defined CSS */"))},









	id.EmojiZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyCreated:    "20210504175807",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {
		constHeader{
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",
			api.KeyModified:   "20231129111800",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:                 "New Zettel",
			api.KeyRole:                  api.ValueRoleConfiguration,
			api.KeySyntax:                meta.SyntaxZmk,
			api.KeyCreated:               "20201028185209",
			api.KeyModified:              "20230929132900",
			meta.NewPrefix + api.KeyRole: api.ValueRoleZettel,
			api.KeyVisibility:            api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewRole): {
		constHeader{
			api.KeyTitle:                  "New Role",
			api.KeyRole:                   api.ValueRoleConfiguration,
			api.KeySyntax:                 meta.SyntaxZmk,
			api.KeyCreated:                "20231129110800",
			meta.NewPrefix + api.KeyRole:  api.ValueRoleRole,
			meta.NewPrefix + api.KeyTitle: "",
			api.KeyVisibility:             api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewTag): {
		constHeader{
			api.KeyTitle:                  "New Tag",
			api.KeyRole:                   api.ValueRoleConfiguration,
			api.KeySyntax:                 meta.SyntaxZmk,
			api.KeyCreated:                "20230929132400",
			meta.NewPrefix + api.KeyRole:  api.ValueRoleTag,
			meta.NewPrefix + api.KeyTitle: "#",
			api.KeyVisibility:             api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewUser): {
		constHeader{
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.ValueRoleConfiguration,
			api.KeySyntax:                      meta.SyntaxNone,
			api.KeyCreated:                     "20201028185209",
			meta.NewPrefix + api.KeyCredential: "",
			meta.NewPrefix + api.KeyUserID:     "",
			meta.NewPrefix + api.KeyUserRole:   api.ValueUserRoleReader,
			api.KeyVisibility:                  api.ValueVisibilityOwner,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidRoleZettelZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleZettel,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129161400",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleZettel)},
	id.MustParse(api.ZidRoleConfigurationZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleConfiguration,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162800",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleConfiguration)},
	id.MustParse(api.ZidRoleRoleZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleRole,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162900",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleRole)},
	id.MustParse(api.ZidRoleTagZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleTag,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162000",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleTag)},
	id.DefaultHomeZid: {
		constHeader{
			api.KeyTitle:   "Home",
			api.KeyRole:    api.ValueRoleZettel,
			api.KeySyntax:  meta.SyntaxZmk,
			api.KeyLang:    api.ValueLangEN,
			api.KeyCreated: "20210210190757",
392
393
394
395
396
397
398



399
400



401
402
403
404
405
406
407
408
409












410
411
412

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

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




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




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

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

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













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







>
>
>

|
>
>
>









>
>
>
>
>
>
>
>
>
>
>
>



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

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

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

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

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

//go:embed prelude.sxn
var contentPreludeSxn []byte

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

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

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

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

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

//go:embed rolerole.zettel
var contentRoleRole []byte

//go:embed roletag.zettel
var contentRoleTag []byte

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

Changes to box/constbox/form.sxn.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
`(article
  (header (h1 ,heading))
  (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data"))
  (div
    (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") (placeholder "Title..") (value ,meta-title) (autofocus))))
  (div
    (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role)
      ,@(if role-data '((list "zs-role-data")))
    ))
    ,@(wui-datalist "zs-role-data" role-data)
  )
  (div
    (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") (placeholder "#tag") (value ,meta-tags))))
  (div
    (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "&#9432;")))
    (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") (placeholder "metakey: metavalue")) ,meta))
  (div
    (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax)
      ,@(if syntax-data '((list "zs-syntax-data")))
    ))
    ,@(wui-datalist "zs-syntax-data" syntax-data)
  )
  ,@(if (bound? 'content)
    `((div
      (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (placeholder "Zettel content..")) ,content)
    ))
  )
  (div
    (input (@ (class "zs-primary") (type "submit") (value "Submit")))
    (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save")))
    (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file")))
  ))
)





|


|






|


|


|







|








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
`(article
  (header (h1 ,heading))
  (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data"))
  (div
    (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus))))
  (div
    (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role) (dir "auto")
      ,@(if role-data '((list "zs-role-data")))
    ))
    ,@(wui-datalist "zs-role-data" role-data)
  )
  (div
    (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") (placeholder "#tag") (value ,meta-tags) (dir "auto"))))
  (div
    (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "&#9432;")))
    (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") (placeholder "metakey: metavalue") (dir "auto")) ,meta))
  (div
    (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax) (dir "auto")
      ,@(if syntax-data '((list "zs-syntax-data")))
    ))
    ,@(wui-datalist "zs-syntax-data" syntax-data)
  )
  ,@(if (bound? 'content)
    `((div
      (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (placeholder "Zettel content..") (dir "auto")) ,content)
    ))
  )
  (div
    (input (@ (class "zs-primary") (type "submit") (value "Submit")))
    (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save")))
    (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file")))
  ))
)

Changes to box/constbox/info.sxn.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      ,@(if (bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      ,@(if (bound? 'copy-url) `((@H " &#183; ") (a (@ (href ,copy-url)) "Copy")))
      ,@(if (bound? 'version-url) `((@H " &#183; ") (a (@ (href ,version-url)) "Version")))
      ,@(if (bound? 'child-url) `((@H " &#183; ") (a (@ (href ,child-url)) "Child")))
      ,@(if (bound? 'folge-url) `((@H " &#183; ") (a (@ (href ,folge-url)) "Folge")))
      ,@(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-table-row metadata))
  (h2 "References")






|
<
|
<







1
2
3
4
5
6
7

8

9
10
11
12
13
14
15
`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      ,@(if (bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      ,@(ROLE-DEFAULT-actions (current-environment))

      ,@(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-table-row metadata))
  (h2 "References")

Changes to box/constbox/listzettel.sxn.

1
2
3
4












5
6
7
8
9
10
11
`(article
  (header (h1 ,heading))
  (form (@ (action ,search-url))
    (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value))))












  ,@content
  ,@endnotes
  (form (@ (action ,(if (bound? 'create-url) create-url)))
      "Other encodings: "
      (a (@ (href ,data-url)) "data")
      ", "
      (a (@ (href ,plain-url)) "plain")



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







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
`(article
  (header (h1 ,heading))
  (form (@ (action ,search-url))
    (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value) (dir "auto"))))
  ,@(if (bound? 'tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel))
    )
  ,@(if (bound? 'create-tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel))
    )
  ,@(if (bound? 'role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel))
    )
  ,@(if (bound? 'create-role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel))
    )
  ,@content
  ,@endnotes
  (form (@ (action ,(if (bound? 'create-url) create-url)))
      "Other encodings: "
      (a (@ (href ,data-url)) "data")
      ", "
      (a (@ (href ,plain-url)) "plain")

Changes to box/constbox/newtoc.zettel.

1
2
3


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


* [[New User|00000000090002]]



>
>

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

Added box/constbox/prelude.sxn.



























































































































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

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

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

;; Function not
(defun not (x) (if x NIL T))
(defconst not not)

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

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

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

;; and macro
;;
;; (and EXPR ...)
(defmacro and args
    (cond ((null? args)       T)
          ((null? (cdr args)) (car args))
          (T                  `(if ,(car args) (and ,@(cdr args))))))


;; or macro
;;
;; (or EXPR ...)
(defmacro or args
    (cond ((null? args)       NIL)
          ((null? (cdr args)) (car args))
          (T                  `(if ,(car args) T (or ,@(cdr args))))))

Added box/constbox/roleconfiguration.zettel.









































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

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

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

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

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

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

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

Added box/constbox/rolerole.zettel.





















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

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

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

Added box/constbox/roletag.zettel.













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

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

Added box/constbox/rolezettel.zettel.















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

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

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

Added box/constbox/start.sxn.





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
;;;----------------------------------------------------------------------------
;;; 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.
;;;----------------------------------------------------------------------------

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

Changes to box/constbox/wuicode.sxn.

1
2
3
4
5
6
7
8
9
10


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
























































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



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

;; wui-table-row takes a pair and translates it into a HTML table row with
;; two columns.
(define (wui-table-row p)
    `(tr (td ,(car p)) (td ,(cdr p))))

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

;; wui-link taks an url and returns a HTML link inside.
(define (wui-link q)
    `(a (@ (href ,(cdr q))) ,(car q)))

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

;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside
;; a table data item.
(define (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.
(define (wui-item-popup-link e)
    `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e)))

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

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

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

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

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


































































>
>

|



|




|




|
|




|



|



|



|



|





|



|



|




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

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

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

;; wui-table-row takes a pair and translates it into a HTML table row with
;; two columns.
(defunconst wui-table-row (p)
    `(tr (td ,(car p)) (td ,(cdr p))))

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

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

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

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

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

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

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

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

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

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

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

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

;;; ACTION-SEPARATOR defines a HTML value that separates actions links.
(defvar ACTION-SEPARATOR '(@H " &#183; "))

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

;;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag".
(defunconst ROLE-tag-actions (env)
    `(,@(ROLE-DEFAULT-actions env)
      ,@(let ((title (environment-lookup 'title env)))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (string-append "tags:" title)))) "Zettel"))
             )
        )
    )
)

;;; ROLE-DEFAULT-heading returns the default text for headings, below the
;;; references of a zettel. In most cases it should be called from an
;;; overwriting function.
(defunconst ROLE-DEFAULT-heading (env)
    `(,@(let ((meta-url (environment-lookup 'meta-url env)))
           (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url))))
      ,@(let ((meta-author (environment-lookup 'meta-author env)))
           (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author)))
    )
)

Changes to box/constbox/zettel.sxn.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
`(article
  (header
    (h1 ,heading)
    (div (@ (class "zs-meta"))
      ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " &#183; ")))
      ,zid (@H " &#183; ")
      (a (@ (href ,info-url)) "Info") (@H " &#183; ")
      "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role)))
          ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      ,@(if (bound? 'copy-url) `((@H " &#183; ") (a (@ (href ,copy-url)) "Copy")))
      ,@(if (bound? 'version-url) `((@H " &#183; ") (a (@ (href ,version-url)) "Version")))
      ,@(if (bound? 'child-url) `((@H " &#183; ") (a (@ (href ,child-url)) "Child")))
      ,@(if (bound? 'folge-url) `((@H " &#183; ") (a (@ (href ,folge-url)) "Folge")))
      ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
      ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
      ,@(if superior-refs `((br) "Superior: " ,superior-refs))
      ,@(if (bound? 'meta-url) `((br) "URL: " ,(url-to-html meta-url)))
      ,@(let (author (and (bound? 'meta-author) meta-author)) (if author `((br) "By " ,author)))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links subordinate-links back-links successor-links)
    `((nav
      ,@(if folge-links `((details (@ (open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
      ,@(if subordinate-links `((details (@ (open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links)))))
      ,@(if back-links `((details (@ (open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
      ,@(if successor-links `((details (@ (open)) (summary "Successors") (ul ,@(map wui-item-link successor-links)))))
     ))
  )
)












|
<
<
<



|
<






|
|
|
|



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



14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
`(article
  (header
    (h1 ,heading)
    (div (@ (class "zs-meta"))
      ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " &#183; ")))
      ,zid (@H " &#183; ")
      (a (@ (href ,info-url)) "Info") (@H " &#183; ")
      "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role)))
          ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      ,@(ROLE-DEFAULT-actions (current-environment))



      ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
      ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
      ,@(if superior-refs `((br) "Superior: " ,superior-refs))
      ,@(ROLE-DEFAULT-heading (current-environment))

    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links subordinate-links back-links successor-links)
    `((nav
      ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
      ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links)))))
      ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
      ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links)))))
     ))
  )
)

Changes to box/dirbox/dirbox.go.

245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
	dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel")
	return meta.Zid, err
}

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







|







245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
	dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel")
	return meta.Zid, err
}

func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	m, c, err := dp.srvGetMetaContent(ctx, entry, zid)
	if err != nil {
		return zettel.Zettel{}, err
	}
	zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)}
	dp.log.Trace().Zid(zid).Msg("GetZettel")
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
	if dp.readonly {
		return box.ErrReadOnly
	}

	meta := zettel.Meta
	zid := meta.Zid
	if !zid.IsValid() {
		return &box.ErrInvalidID{Zid: zid}
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		// Existing zettel, but new in this box.
		entry = &notify.DirEntry{Zid: zid}
	}
	dp.updateEntryFromMetaContent(entry, meta, zettel.Content)







|







298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
	if dp.readonly {
		return box.ErrReadOnly
	}

	meta := zettel.Meta
	zid := meta.Zid
	if !zid.IsValid() {
		return box.ErrInvalidZid{Zid: zid.String()}
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		// Existing zettel, but new in this box.
		entry = &notify.DirEntry{Zid: zid}
	}
	dp.updateEntryFromMetaContent(entry, meta, zettel.Content)
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351

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.ErrNotFound
	}
	if dp.readonly {
		return box.ErrReadOnly
	}

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

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








|







|







329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351

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
	}

380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if dp.readonly {
		return box.ErrReadOnly
	}

	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return box.ErrNotFound
	}
	err := dp.dirSrv.DeleteDirEntry(zid)
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {







|







380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if dp.readonly {
		return box.ErrReadOnly
	}

	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return box.ErrZettelNotFound{Zid: zid}
	}
	err := dp.dirSrv.DeleteDirEntry(zid)
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {

Changes to box/dirbox/service.go.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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







|
|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

Changes to box/filebox/zipbox.go.

78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
}

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

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

func (zb *zipBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) {
	err := box.ErrReadOnly
	zb.log.Trace().Err(err).Msg("CreateZettel")
	return id.Invalid, err
}

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








<
<
<
<
<
<
<
<



|







78
79
80
81
82
83
84








85
86
87
88
89
90
91
92
93
94
95
}

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









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

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
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}

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

func (zb *zipBox) UpdateZettel(context.Context, zettel.Zettel) error {
	err := box.ErrReadOnly
	zb.log.Trace().Err(err).Msg("UpdateZettel")
	return err
}

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.ErrNotFound
	}
	zb.log.Trace().Err(err).Msg("RenameZettel")
	return err
}

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

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

func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true







<
<
<
<
<
<
<
<












|











|







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
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}









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

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

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

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

func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true

Changes to box/manager/anteroom.go.

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

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

144
145
146
147
148
149
150
151
152
const (
	arNothing arAction = iota
	arReload
	arZettel
)

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

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

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

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

func (ar *anterooms) makeAnteroom(zid id.Zid) *anteroom {
	ar.nextNum++
	if zid == id.Invalid {
		return &anteroom{num: ar.nextNum, next: nil, waiting: nil, curLoad: 0, reload: true}
	}
	c := ar.maxLoad
	if c == 0 {
		c = 100
	}
	waiting := id.NewSetCap(ar.maxLoad, zid)
	return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false}
}

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

func (ar *anterooms) Reload(newZids id.Set) uint64 {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.deleteReloadedRooms()

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

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


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

func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		return arNothing, id.Invalid, 0
	}
	roomNo := ar.first.num
	if ar.first.waiting == nil {
		ar.removeFirst()
		return arReload, id.Invalid, roomNo
	}
	for zid := range ar.first.waiting {
		delete(ar.first.waiting, zid)
		if len(ar.first.waiting) == 0 {
			ar.removeFirst()
		}
		return arZettel, zid, roomNo
	}
	ar.removeFirst()

	return arNothing, id.Invalid, 0
}

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







<






|

<





|

|




















|








|
<

<
<
<
<
|

|
|


|


|



|




|
<
|



<
<
|
|
|
<
|
|
>
|










|


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


|





21
22
23
24
25
26
27

28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

74




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

94
95
96
97


98
99
100

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const (
	arNothing arAction = iota
	arReload
	arZettel
)

type anteroom struct {

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

type anteroomQueue struct {
	mx      sync.Mutex

	first   *anteroom
	last    *anteroom
	maxLoad int
}

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

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

func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom {

	if zid == id.Invalid {




		panic(zid)
	}
	waiting := id.NewSetCap(max(ar.maxLoad, 100), zid)
	return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false}
}

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

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

	if ns := len(allZids); ns > 0 {

		ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: ns, reload: true}
		if ar.first.next == nil {
			ar.last = ar.first
		}


	} else {
		ar.first = nil
		ar.last = nil

	}
}

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

func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	first := ar.first


	if first != nil {
		if first.waiting == nil && first.reload {
			ar.removeFirst()
			return arReload, id.Invalid, false
		}
		for zid := range first.waiting {
			delete(first.waiting, zid)
			if len(first.waiting) == 0 {
				ar.removeFirst()
			}
			return arZettel, zid, first.reload
		}
		ar.removeFirst()
	}
	return arNothing, id.Invalid, false
}

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

Changes to box/manager/anteroom_test.go.

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

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

func TestSimple(t *testing.T) {
	t.Parallel()
	ar := newAnterooms(2)
	ar.EnqueueZettel(id.Zid(1))
	action, zid, rno := ar.Dequeue()
	if zid != id.Zid(1) || action != arZettel || rno != 1 {
		t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno)
	}
	_, zid, _ = ar.Dequeue()
	if zid != id.Invalid {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.EnqueueZettel(id.Zid(1))
	ar.EnqueueZettel(id.Zid(2))







|

|
|
|







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

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

func TestSimple(t *testing.T) {
	t.Parallel()
	ar := newAnteroomQueue(2)
	ar.EnqueueZettel(id.Zid(1))
	action, zid, lastReload := ar.Dequeue()
	if zid != id.Zid(1) || action != arZettel || lastReload {
		t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload)
	}
	_, zid, _ = ar.Dequeue()
	if zid != id.Invalid {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.EnqueueZettel(id.Zid(1))
	ar.EnqueueZettel(id.Zid(2))
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	t.Parallel()
	ar := newAnterooms(1)
	ar.EnqueueZettel(id.Zid(1))
	ar.Reset()
	action, zid, _ := ar.Dequeue()
	if action != arReload || zid != id.Invalid {
		t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
	}
	ar.Reload(id.NewSet(3, 4))







|







48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	t.Parallel()
	ar := newAnteroomQueue(1)
	ar.EnqueueZettel(id.Zid(1))
	ar.Reset()
	action, zid, _ := ar.Dequeue()
	if action != arReload || zid != id.Invalid {
		t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
	}
	ar.Reload(id.NewSet(3, 4))
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
		t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action)
	}
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

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

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







|










|







81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
		t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action)
	}
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

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

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

Changes to box/manager/box.go.

37
38
39
40
41
42
43



44
45

46


47
48
49
50
51
52
53
54
55
56


57






58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
		sb.WriteString(mgr.boxes[i].Location())
	}
	return sb.String()
}

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



	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()

	return mgr.State() == box.StartStateStarted && mgr.boxes[0].CanCreateZettel(ctx)


}

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


	return mgr.boxes[0].CreateZettel(ctx, zettel)






}

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

		if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, z.Meta, i+1)
			}
			return z, err
		}
	}
	return zettel.Zettel{}, box.ErrNotFound
}

// GetAllZettel retrieves a specific zettel from all managed boxes.
func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel")
	if mgr.State() != box.StartStateStarted {
		return nil, box.ErrStopped







>
>
>


>
|
>
>










>
>
|
>
>
>
>
>
>











>
|






|







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
		sb.WriteString(mgr.boxes[i].Location())
	}
	return sb.String()
}

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

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

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

// GetAllZettel retrieves a specific zettel from all managed boxes.
func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel")
	if mgr.State() != box.StartStateStarted {
		return nil, box.ErrStopped
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
	if mgr.State() != box.StartStateStarted {
		return nil, box.ErrStopped
	}
	result := id.Set{}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Zid(zid) }, func(id.Zid) bool { return true })
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}








|







115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
	if mgr.State() != box.StartStateStarted {
		return nil, box.ErrStopped
	}
	result := id.Set{}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, func(id.Zid) bool { return true })
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

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

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



	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()

	return mgr.State() == box.StartStateStarted && mgr.boxes[0].CanUpdateZettel(ctx, zettel)



}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	// Remove all (computed) properties from metadata before storing the zettel.
	zettel.Meta = zettel.Meta.Clone()
	for _, p := range zettel.Meta.ComputedPairsRest() {
		if mgr.propertyKeys.Has(p.Key) {
			zettel.Meta.Delete(p.Key)
		}


	}
	return mgr.boxes[0].UpdateZettel(ctx, zettel)
}

// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	if mgr.State() != box.StartStateStarted {
		return false
	}







|











|




















>
>
>


>
|
>
>
>








|
|
|
<
|

>
>

|







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

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

}

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

			return err
		}
		mgr.idxUpdateZettel(ctx, zettel)
		return nil
	}
	return box.ErrReadOnly
}

// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	if mgr.State() != box.StartStateStarted {
		return false
	}
238
239
240
241
242
243
244

245
246
247
248
249
250
251

252
253
254
255
256
257
258
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for i, p := range mgr.boxes {
		err := p.RenameZettel(ctx, curZid, newZid)

		if err != nil && !errors.Is(err, box.ErrNotFound) {
			for j := 0; j < i; j++ {
				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}

	return nil
}

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







>
|






>







261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for i, p := range mgr.boxes {
		err := p.RenameZettel(ctx, curZid, newZid)
		var errZNF box.ErrZettelNotFound
		if err != nil && !errors.As(err, &errZNF) {
			for j := 0; j < i; j++ {
				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}
	mgr.idxRenameZettel(ctx, curZid, newZid)
	return nil
}

// CanDeleteZettel returns true, if box could possibly delete the given zettel.
func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	if mgr.State() != box.StartStateStarted {
		return false
274
275
276
277
278
279
280

281
282

283
284
285
286
287
288











		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zid)
		if err == nil {

			return nil
		}

		if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrNotFound
}


















>


>
|



|

>
>
>
>
>
>
>
>
>
>
>
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
		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zid)
		if err == nil {
			mgr.idxDeleteZettel(ctx, zid)
			return nil
		}
		var errZNF box.ErrZettelNotFound
		if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrZettelNotFound{Zid: zid}
}

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

Changes to box/manager/collect.go.

72
73
74
75
76
77
78
79
80
81
	if ref.IsExternal() {
		data.urls.Add(strings.ToLower(ref.Value))
	}
	if !ref.IsZettel() {
		return
	}
	if zid, err := id.Parse(ref.URL.Path); err == nil {
		data.refs.Zid(zid)
	}
}







|


72
73
74
75
76
77
78
79
80
81
	if ref.IsExternal() {
		data.urls.Add(strings.ToLower(ref.Value))
	}
	if !ref.IsZettel() {
		return
	}
	if zid, err := id.Parse(ref.URL.Path); err == nil {
		data.refs.Add(zid)
	}
}

Changes to box/manager/indexer.go.

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

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

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

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

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

			mgr.idxMx.Lock()
			if arRoomNum == roomNum {
				mgr.idxDurReload = time.Since(start)
			}
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()
			mgr.idxUpdateZettel(ctx, zettel)
		}
	}
}

func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool {
	select {
	case _, ok := <-mgr.idxReady:







|
|
















<


|




<



|
<
<











<
<
<
|



>

|




<







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

96
97
98
99
100
101
102

103
104
105
106


107
108
109
110
111
112
113
114
115
116
117



118
119
120
121
122
123
124
125
126
127
128

129
130
131
132
133
134
135
}

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

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

func (mgr *Manager) idxWorkService(ctx context.Context) {

	var start time.Time
	for {
		switch action, zid, lastReload := mgr.idxAr.Dequeue(); action {
		case arNothing:
			return
		case arReload:
			mgr.idxLog.Debug().Msg("reload")

			zids, err := mgr.FetchZids(ctx)
			if err == nil {
				start = time.Now()
				mgr.idxAr.Reload(zids)


				mgr.idxMx.Lock()
				mgr.idxLastReload = time.Now().Local()
				mgr.idxSinceReload = 0
				mgr.idxMx.Unlock()
			}
		case arZettel:
			mgr.idxLog.Debug().Zid(zid).Msg("zettel")
			zettel, err := mgr.GetZettel(ctx, zid)
			if err != nil {
				// Zettel was deleted or is not accessible b/c of other reasons
				mgr.idxLog.Trace().Zid(zid).Msg("delete")



				mgr.idxDeleteZettel(ctx, zid)
				continue
			}
			mgr.idxLog.Trace().Zid(zid).Msg("update")
			mgr.idxUpdateZettel(ctx, zettel)
			mgr.idxMx.Lock()
			if lastReload {
				mgr.idxDurReload = time.Since(start)
			}
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()

		}
	}
}

func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool {
	select {
	case _, ok := <-mgr.idxReady:
187
188
189
190
191
192
193

194
195



196
197
198
199










200
201
202
203
204
205
206
			is := parser.ParseMetadata(pair.Value)
			collectInlineIndexData(&is, cData)
		case meta.TypeURL:
			if _, err := url.Parse(pair.Value); err == nil {
				cData.urls.Add(pair.Value)
			}
		default:

			for _, word := range strfun.NormalizeWords(pair.Value) {
				cData.words.Add(word)



			}
		}
	}
}











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







>
|
|
>
>
>




>
>
>
>
>
>
>
>
>
>







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
			is := parser.ParseMetadata(pair.Value)
			collectInlineIndexData(&is, cData)
		case meta.TypeURL:
			if _, err := url.Parse(pair.Value); err == nil {
				cData.urls.Add(pair.Value)
			}
		default:
			if descr.Type.IsSet {
				for _, val := range meta.ListFromValue(pair.Value) {
					idxCollectMetaValue(cData.words, val)
				}
			} else {
				idxCollectMetaValue(cData.words, pair.Value)
			}
		}
	}
}

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

func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
	for ref := range cData.refs {
		if mgr.HasZettel(ctx, ref) {
			zi.AddBackRef(ref)
		} else {
			zi.AddDeadRef(ref)
222
223
224
225
226
227
228





229
230
231
232
233
234
235
236
237
238
	if inverseKey == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddInverseRef(inverseKey, zid)
}






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

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







>
>
>
>
>
|
|








229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
	if inverseKey == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddInverseRef(inverseKey, zid)
}

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

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

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

Changes to box/manager/manager.go.

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys strfun.Set // Set of property key names

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

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







|







95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys strfun.Set // Set of property key names

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

	// Indexer stats data
	idxMx          sync.RWMutex
	idxLastReload  time.Time
	idxDurReload   time.Duration
	idxSinceReload uint64
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
		mgrLog:       boxLog.Clone().Str("box", "manager").Child(),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

		idxLog:   boxLog.Clone().Str("box", "index").Child(),
		idxStore: memstore.New(),
		idxAr:    newAnterooms(1000),
		idxReady: make(chan struct{}, 1),
	}
	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {







|







136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
		mgrLog:       boxLog.Clone().Str("box", "manager").Child(),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

		idxLog:   boxLog.Clone().Str("box", "index").Child(),
		idxStore: memstore.New(),
		idxAr:    newAnteroomQueue(1000),
		idxReady: make(chan struct{}, 1),
	}
	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
		mgr.mxObserver.Unlock()
	}
}

func (mgr *Manager) notifier() {
	// The call to notify may panic. Ensure a running notifier.
	defer func() {
		if r := recover(); r != nil {
			kernel.Main.LogRecover("Notifier", r)
			go mgr.notifier()
		}
	}()

	tsLastEvent := time.Now()
	cache := destutterCache{}
	for {







|
|







179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
		mgr.mxObserver.Unlock()
	}
}

func (mgr *Manager) notifier() {
	// The call to notify may panic. Ensure a running notifier.
	defer func() {
		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("Notifier", ri)
			go mgr.notifier()
		}
	}()

	tsLastEvent := time.Now()
	cache := destutterCache{}
	for {
352
353
354
355
356
357
358

359
360
361
362
363
364
365
366
367
368










369
370
371
372
373
374
375

// Refresh internal box data.
func (mgr *Manager) Refresh(ctx context.Context) error {
	mgr.mgrLog.Debug().Msg("Refresh")
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}

	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil
}











// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	mgr.mgrLog.Debug().Msg("ReadStats")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	subStats := make([]box.ManagedBoxStats, len(mgr.boxes))







>


<







>
>
>
>
>
>
>
>
>
>







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

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

	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil
}

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

// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	mgr.mgrLog.Debug().Msg("ReadStats")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	subStats := make([]box.ManagedBoxStats, len(mgr.boxes))

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

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

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

type zettelData struct {
	meta      *meta.Meta
	dead      id.Slice
	forward   id.Slice
	backward  id.Slice
	otherRefs map[string]bidiRefs
	words     []string
	urls      []string
}

type stringRefs map[string]id.Slice

type memStore struct {
	mx     sync.RWMutex
	intern map[string]string // map to intern strings







|
|
|
|

|
|







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

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

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

type stringRefs map[string]id.Slice

type memStore struct {
	mx     sync.RWMutex
	intern map[string]string // map to intern strings
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83
84
		urls:   make(stringRefs),
	}
}

func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	if zi, found := ms.idx[zid]; found {

		return zi.meta.Clone(), nil
	}
	return nil, box.ErrNotFound
}

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







|
>


|







67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
		urls:   make(stringRefs),
	}
}

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

func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) {
	if ms.doEnrich(m) {
		ms.mxStats.Lock()
		ms.updates++
		ms.mxStats.Unlock()
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
		return false
	}
	var updated bool
	if len(zi.dead) > 0 {
		m.Set(api.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Copy())
	if len(zi.backward) > 0 {
		m.Set(api.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(api.KeyForward, zi.forward.String())
		back = remRefs(back, zi.forward)







|







94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
		return false
	}
	var updated bool
	if len(zi.dead) > 0 {
		m.Set(api.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Clone())
	if len(zi.backward) > 0 {
		m.Set(api.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(api.KeyForward, zi.forward.String())
		back = remRefs(back, zi.forward)
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchEqual(word string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := id.NewSet()
	if refs, ok := ms.words[word]; ok {
		result.AddSlice(refs)
	}
	if refs, ok := ms.urls[word]; ok {
		result.AddSlice(refs)
	}
	zid, err := id.Parse(word)
	if err != nil {
		return result
	}
	zi, ok := ms.idx[zid]
	if !ok {







|


|







125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchEqual(word string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := id.NewSet()
	if refs, ok := ms.words[word]; ok {
		result.CopySlice(refs)
	}
	if refs, ok := ms.urls[word]; ok {
		result.CopySlice(refs)
	}
	zid, err := id.Parse(word)
	if err != nil {
		return result
	}
	zi, ok := ms.idx[zid]
	if !ok {
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
func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set {
	// Must only be called if ms.mx is read-locked!
	result := id.NewSet()
	for word, refs := range ms.words {
		if !pred(word, s) {
			continue
		}
		result.AddSlice(refs)
	}
	for u, refs := range ms.urls {
		if !pred(u, s) {
			continue
		}
		result.AddSlice(refs)
	}
	return result
}

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

func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
	for _, p := range m.PairsRest() {
		switch meta.Type(p.Key) {
		case meta.TypeID:







|





|






|
|

|







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

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

func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
	for _, p := range m.PairsRest() {
		switch meta.Type(p.Key) {
		case meta.TypeID:
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
			}
		}
	}
	return back
}

func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set {
	m := ms.makeMeta(zidx)
	ms.mx.Lock()
	defer ms.mx.Unlock()

	zi, ziExist := ms.idx[zidx.Zid]
	if !ziExist || zi == nil {
		zi = &zettelData{}
		ziExist = false
	}

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

	zi.meta = m
	ms.updateDeadReferences(zidx, zi)
	ms.updateForwardBackwardReferences(zidx, zi)

	ms.updateMetadataReferences(zidx, zi)

	zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords())
	zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls())

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

	return toCheck







<


>
















|
>
|
>
|
|







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

func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set {

	ms.mx.Lock()
	defer ms.mx.Unlock()
	m := ms.makeMeta(zidx)
	zi, ziExist := ms.idx[zidx.Zid]
	if !ziExist || zi == nil {
		zi = &zettelData{}
		ziExist = false
	}

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

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

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

	return toCheck
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
		ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid)
	}
	for _, ref := range newRefs {
		ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid)
	}
}

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


	for _, ref := range remRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)


	}

	for _, ref := range newRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)


	}
}



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

	for key, mrefs := range inverseRefs {
		mr := zi.otherRefs[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.otherRefs[key] = mr

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



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

}

func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {
		if refs, ok := srefs[word]; ok {
			srefs[word] = addRef(refs, zid)
			continue
		}
		srefs[word] = id.Slice{zid}







|




>
>

|

>
>
|
>

|

>
>
|
|
>
|
>
|











>







|






>
>
>



>


|







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
		ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid)
	}
	for _, ref := range newRefs {
		ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid)
	}
}

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

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

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

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

func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {
		if refs, ok := srefs[word]; ok {
			srefs[word] = addRef(refs, zid)
			continue
		}
		srefs[word] = id.Slice{zid}
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
			continue
		}
		srefs[word] = refs2
	}
	return next.Words()
}

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







































































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





	zi, ok := ms.idx[zid]
	if !ok {
		return nil
	}

	ms.deleteDeadSources(zid, zi)
	toCheck := ms.deleteForwardBackward(zid, zi)
	if len(zi.otherRefs) > 0 {
		for key, mrefs := range zi.otherRefs {
			ms.removeInverseMeta(zid, key, mrefs.forward)
		}
	}
	ms.deleteWords(zid, zi.words)

	delete(ms.idx, zid)
	return toCheck
}

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

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

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

func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) {
	// Must only be called if ms.mx is write-locked!







|









>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



>
>

>
>







<
|
|
|
<
|
>




















<





>



<
|
<
<







434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535

536
537
538

539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560

561
562
563
564
565
566
567
568
569

570


571
572
573
574
575
576
577
			continue
		}
		srefs[word] = refs2
	}
	return next.Words()
}

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

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

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

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

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

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

	ms.deleteDeadSources(zid, zi)
	toCheck := ms.deleteForwardBackward(zid, zi)

	for key, mrefs := range zi.otherRefs {
		ms.removeInverseMeta(zid, key, mrefs.forward)
	}

	deleteStrings(ms.words, zi.words, zid)
	deleteStrings(ms.urls, zi.urls, zid)
	delete(ms.idx, zid)
	return toCheck
}

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

func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set {
	// Must only be called if ms.mx is write-locked!

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

			toCheck = toCheck.Add(ref)


		}
	}
	return toCheck
}

func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) {
	// Must only be called if ms.mx is write-locked!
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
			if len(bzi.otherRefs) == 0 {
				bzi.otherRefs = nil
			}
		}
	}
}

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

func (ms *memStore) ReadStats(st *store.Stats) {
	ms.mx.RLock()
	st.Zettel = len(ms.idx)
	st.Words = uint64(len(ms.words))







|

|
|





|


|







592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
			if len(bzi.otherRefs) == 0 {
				bzi.otherRefs = nil
			}
		}
	}
}

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

func (ms *memStore) ReadStats(st *store.Stats) {
	ms.mx.RLock()
	st.Zettel = len(ms.idx)
	st.Words = uint64(len(ms.words))

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

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



13

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

package memstore




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


func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) {
	npos, opos := 0, 0
	for npos < len(refsN) && opos < len(refsO) {
		rn, ro := refsN[npos], refsO[opos]
		if rn == ro {
			npos++












>
>
>
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//-----------------------------------------------------------------------------
// Copyright (c) 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.
//-----------------------------------------------------------------------------

package memstore

import (
	"slices"

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

func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) {
	npos, opos := 0, 0
	for npos < len(refsN) && opos < len(refsO) {
		rn, ro := refsN[npos], refsO[opos]
		if rn == ro {
			npos++
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
			return refs
		} else if r < ref {
			lo = m + 1
		} else {
			hi = m
		}
	}
	refs = append(refs, id.Invalid)
	copy(refs[hi+1:], refs[hi:])
	refs[hi] = ref
	return refs
}

func remRefs(refs, rem id.Slice) id.Slice {
	if len(refs) == 0 || len(rem) == 0 {
		return refs
	}







|
<
<







50
51
52
53
54
55
56
57


58
59
60
61
62
63
64
			return refs
		} else if r < ref {
			lo = m + 1
		} else {
			hi = m
		}
	}
	refs = slices.Insert(refs, hi, ref)


	return refs
}

func remRefs(refs, rem id.Slice) id.Slice {
	if len(refs) == 0 || len(rem) == 0 {
		return refs
	}

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

45
46
47
48
49
50
51




52
53
54
55
56
57
58
59
60
61
62

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

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





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

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

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







>
>
>
>











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

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

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

	// RenameZettel changes all references of current zettel identifier to new
	// zettel identifier.
	RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set

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

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

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

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

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
		deadrefs:    id.NewSet(),
	}
}

// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
	zi.backrefs.Zid(zid)
}

// AddInverseRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) {
	if zids, ok := zi.inverseRefs[key]; ok {
		zids.Zid(zid)
		return
	}
	zi.inverseRefs[key] = id.NewSet(zid)
}

// AddDeadRef adds a dead reference to a zettel.
func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
	zi.deadrefs.Zid(zid)
}

// SetWords sets the words to the given value.
func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words }

// SetUrls sets the words to the given value.
func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls }







|






|







|







36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
		deadrefs:    id.NewSet(),
	}
}

// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
	zi.backrefs.Add(zid)
}

// AddInverseRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) {
	if zids, ok := zi.inverseRefs[key]; ok {
		zids.Add(zid)
		return
	}
	zi.inverseRefs[key] = id.NewSet(zid)
}

// AddDeadRef adds a dead reference to a zettel.
func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
	zi.deadrefs.Add(zid)
}

// SetWords sets the words to the given value.
func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words }

// SetUrls sets the words to the given value.
func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls }

Changes to box/membox/membox.go.

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
}

func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	mb.mx.RLock()
	z, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	if !ok {
		return zettel.Zettel{}, box.ErrNotFound
	}
	z.Meta = z.Meta.Clone()
	mb.log.Trace().Msg("GetZettel")
	return z, nil
}

func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool {







|







118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
}

func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	mb.mx.RLock()
	z, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	if !ok {
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	z.Meta = z.Meta.Clone()
	mb.log.Trace().Msg("GetZettel")
	return z, nil
}

func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool {
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
	}
	return newBytes < mb.maxBytes
}

func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error {
	m := zettel.Meta.Clone()
	if !m.Zid.IsValid() {
		return &box.ErrInvalidID{Zid: m.Zid}
	}

	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[m.Zid]; found {
		newBytes -= prevZettel.Length()
	}







|







176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
	}
	return newBytes < mb.maxBytes
}

func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error {
	m := zettel.Meta.Clone()
	if !m.Zid.IsValid() {
		return box.ErrInvalidZid{Zid: m.Zid.String()}
	}

	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[m.Zid]; found {
		newBytes -= prevZettel.Length()
	}
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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.ErrNotFound
	}

	// Check that there is no zettel with newZid
	if _, ok = mb.zettel[newZid]; ok {
		mb.mx.Unlock()
		return &box.ErrInvalidID{Zid: newZid}
	}

	meta := zettel.Meta.Clone()
	meta.Zid = newZid
	zettel.Meta = meta
	mb.zettel[newZid] = zettel
	delete(mb.zettel, curZid)







|





|







205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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)
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
}

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.ErrNotFound
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	mb.log.Trace().Msg("DeleteZettel")
	return nil







|







238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
}

func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	mb.mx.Lock()
	oldZettel, found := mb.zettel[zid]
	if !found {
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	mb.log.Trace().Msg("DeleteZettel")
	return nil

Changes to box/notify/directory.go.

187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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.ErrInvalidID{Zid: newZid}
	}
	oldZid := oldEntry.Zid
	newEntry := DirEntry{
		Zid:         newZid,
		MetaName:    renameFilename(oldEntry.MetaName, oldZid, newZid),
		ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid),
		ContentExt:  oldEntry.ContentExt,







|







187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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,
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
	delete(ds.entries, zid)
	return nil
}

func (ds *DirService) updateEvents(newEntries entrySet) {
	// Something may panic. Ensure a running service.
	defer func() {
		if r := recover(); r != nil {
			kernel.Main.LogRecover("DirectoryService", r)
			go ds.updateEvents(newEntries)
		}
	}()

	for ev := range ds.notifier.Events() {
		e, ok := ds.handleEvent(ev, newEntries)
		if !ok {







|
|







223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
	delete(ds.entries, zid)
	return nil
}

func (ds *DirService) updateEvents(newEntries entrySet) {
	// Something may panic. Ensure a running service.
	defer func() {
		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("DirectoryService", ri)
			go ds.updateEvents(newEntries)
		}
	}()

	for ev := range ds.notifier.Events() {
		e, ok := ds.handleEvent(ev, newEntries)
		if !ok {

Changes to box/notify/fsdir.go.

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

	if ev.Has(fsnotify.Create) {
		err := fsdn.base.Add(fsdn.path)
		if err != nil {
			fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory")
			select {
			case fsdn.events <- Event{Op: Error, Err: err}:
			case <-fsdn.done:
				fsdn.log.Trace().Int("i", 2).Msg("done dir event processing")
				return false
			}
		}







|







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

	if ev.Has(fsnotify.Create) {
		err := fsdn.base.Add(fsdn.path)
		if err != nil {
			fsdn.log.Error().Err(err).Str("name", fsdn.path).Msg("Unable to add directory")
			select {
			case fsdn.events <- Event{Op: Error, Err: err}:
			case <-fsdn.done:
				fsdn.log.Trace().Int("i", 2).Msg("done dir event processing")
				return false
			}
		}

Changes to cmd/cmd_file.go.

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
		zettel.Zettel{
			Meta:    m,
			Content: zettel.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(api.KeySyntax, meta.SyntaxZmk),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc))
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}
	_, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata)
	if err != nil {
		return 2, err







|







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
		zettel.Zettel{
			Meta:    m,
			Content: zettel.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(api.KeySyntax, meta.SyntaxZmk),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc), &encoder.CreateParameter{Lang: m.GetDefault(api.KeyLang, api.ValueLangEN)})
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}
	_, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata)
	if err != nil {
		return 2, err

Changes to cmd/cmd_run.go.

62
63
64
65
66
67
68


69
70
71
72
73
74

75
76
77
78
79
80
81
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucQuery := usecase.NewQuery(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery)
	ucQuery.SetEvaluate(&ucEvaluate)


	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)

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







>
>






>







62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucQuery := usecase.NewQuery(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery)
	ucQuery.SetEvaluate(&ucEvaluate)
	ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery)
	ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery)
	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(),
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
		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))
		webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
	}
	webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
	webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery))
	webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel))
	webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler())
	webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler(
		ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery))

	// API
	webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
	webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion))
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery))
	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))
	}







|











|







102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
		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))
		webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
	}
	webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
	webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel))
	webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler())
	webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler(
		ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery))

	// API
	webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
	webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion))
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename))
	}

Changes to config/config.go.

15
16
17
18
19
20
21
22
23




24
25
26
27
28
29
30
	"context"

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

// Key values that are supported by Config.Get
const (
	KeyFooterZettel = "footer-zettel"
	KeyHomeZettel   = "home-zettel"




	// api.KeyLang
)

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig








|
|
>
>
>
>







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

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

// Key values that are supported by Config.Get
const (
	KeyFooterZettel         = "footer-zettel"
	KeyHomeZettel           = "home-zettel"
	KeyShowBackLinks        = "show-back-links"
	KeyShowFolgeLinks       = "show-folge-links"
	KeyShowSubordinateLinks = "show-subordinate-links"
	KeyShowSuccessorLinks   = "show-successor-links"
	// api.KeyLang
)

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig

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
id: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk

modified: 20220803183647


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



Licensed under the EUPL-1.2-or-later.





>
|
>
















>
>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210301190630
modified: 20231125185455
show-back-links: false

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

Version: {{00001000000001}}.

Licensed under the EUPL-1.2-or-later.

Added docs/manual/00001000000001.zettel.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
id: 00001000000001
title: Manual Version
role: configuration
syntax: zmk
created: 20231002142915
modified: 20231002142948

To be set by build tool.

Changes to docs/manual/00001004020000.zettel.

1
2
3
4
5
6
7

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

23
24
25

26
27
28
29

30
31
32
33
34
35
36
37
38
39
40
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230317183435


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

; [!default-copyright|''default-copyright'']
: Copyright value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''.

  Default: (the empty string).
; [!default-license|''default-license'']
: License value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''.

  Default: (the empty string).
; [!default-visibility|''default-visibility'']
: Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key.

  Default: ""login"".
; [!expert-mode|''expert-mode'']
: If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise).
  This affects most computed zettel.

  Default: ""False"".
; [!footer-zettel|''footer-zettel'']
: Identifier of a zettel that is rendered as HTML and will be placed as the footer of every zettel in the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected.
  If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer.

  May be [[overwritten|00001004020200]] in a user zettel.

  Default: (an invalid zettel identifier)
; [!home-zettel|''home-zettel'']
: Specifies the identifier of the zettel, that should be presented for the default view / home view.






|
>















>



>




>



|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231126180829
show-back-links: false

You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called __configuration zettel__.
The following metadata keys change the appearance / behavior of Zettelstore.
Some of them can be overwritten in an [[user zettel|00001010040200]], a subset of those may be overwritten in zettel that is currently used.
See the full list of [[metadata that may be overwritten|00001004020200]].

; [!default-copyright|''default-copyright'']
: Copyright value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''.

  Default: (the empty string).
; [!default-license|''default-license'']
: License value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''.

  Default: (the empty string).
; [!default-visibility|''default-visibility'']
: Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key.

  Default: ""login"".
; [!expert-mode|''expert-mode'']
: If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise).
  This affects most computed zettel.

  Default: ""False"".
; [!footer-zettel|''footer-zettel'']
: Identifier of a zettel that is rendered as HTML and will be placed as the footer of every zettel in the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as symbolic expressions, etc. is not affected.
  If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer.

  May be [[overwritten|00001004020200]] in a user zettel.

  Default: (an invalid zettel identifier)
; [!home-zettel|''home-zettel'']
: Specifies the identifier of the zettel, that should be presented for the default view / home view.
50
51
52
53
54
55
56

57














58
59
60

61
62
63

64
65
66
67
68
69
70
71
72
73
74
75
  This value is used as a default value, if it is not set in an user's zettel or in a zettel.
  It is also used to specify the language for all non-zettel content, e.g. lists or search results.

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

  Default: ""1024"".














; [!site-name|''site-name'']
: Name of the Zettelstore instance.
  Will be used when displaying some lists.

  Default: ""Zettelstore"".
; [!yaml-header|''yaml-header'']
: If [[true|00001006030500]], metadata and content will be separated by ''---\\n'' instead of an empty line (''\\n\\n'').

  Default: ""False"".

  You will probably use this key, if you are working with another software processing [[Markdown|https://daringfireball.net/projects/markdown/]] that uses a subset of [[YAML|https://yaml.org/]] to specify metadata.
; [!zettel-file-syntax|''zettel-file-syntax'']
: If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files:
  one for the metadata (file without a filename extension) and another for the content (file extension based on the syntax value).
  If you want to specify alternative syntax values, for which you want new zettel to be stored in one file (file extension ''.zettel''), you can use this key.
  All values are case-insensitive, duplicate values are removed.

  For example, you could use this key if you're working with Markdown syntax and you want to store metadata and content in one ''.zettel'' file.

  If ''yaml-header'' evaluates to true, a zettel is always stored in one ''.zettel'' file.







>

>
>
>
>
>
>
>
>
>
>
>
>
>
>



>



>












54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
  This value is used as a default value, if it is not set in an user's zettel or in a zettel.
  It is also used to specify the language for all non-zettel content, e.g. lists or search results.

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

  Default: ""1024"".
; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-subordinate-links|''show-subordinate-links''], [!show-successor-links|''show-successor-links'']
: When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel.
  This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''subordinates''|00001006020000#subordinates]], and  [[''successors''|00001006020000#successors]].

  These configuration keys may be used to show, not to show, or to close the list of referenced zettel.

  Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list).

  Default: """".

  May be [[overwritten|00001004020200]] in a user zettel, so that setting will only affect the given user.
  Alternatively, it may be overwritten in a zettel, so that that the setting will affect only the given zettel.

  This zettel is an example of a zettel that sets ''show-back-links'' to ""false"".
; [!site-name|''site-name'']
: Name of the Zettelstore instance.
  Will be used when displaying some lists.

  Default: ""Zettelstore"".
; [!yaml-header|''yaml-header'']
: If [[true|00001006030500]], metadata and content will be separated by ''---\\n'' instead of an empty line (''\\n\\n'').

  Default: ""False"".

  You will probably use this key, if you are working with another software processing [[Markdown|https://daringfireball.net/projects/markdown/]] that uses a subset of [[YAML|https://yaml.org/]] to specify metadata.
; [!zettel-file-syntax|''zettel-file-syntax'']
: If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files:
  one for the metadata (file without a filename extension) and another for the content (file extension based on the syntax value).
  If you want to specify alternative syntax values, for which you want new zettel to be stored in one file (file extension ''.zettel''), you can use this key.
  All values are case-insensitive, duplicate values are removed.

  For example, you could use this key if you're working with Markdown syntax and you want to store metadata and content in one ''.zettel'' file.

  If ''yaml-header'' evaluates to true, a zettel is always stored in one ''.zettel'' file.

Changes to docs/manual/00001004020200.zettel.

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




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

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










|











>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001004020200
title: Runtime configuration data that may be user specific or zettel specific
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20221205155521
modified: 20231126180752

Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]].
A subset of those may be overwritten in zettel that is currently used.
This allows to specify user specific or zettel specific behavior.

The following metadata keys are supported to provide a more specific behavior:

|=Key|User:|Zettel:|Remarks
|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N|
|[[''home-zettel''|00001004020000#home-zettel]]|Y|N|
|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful
|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y|
|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y|
|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y|
|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y|

Changes to docs/manual/00001005090000.zettel.

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

The following table lists all predefined zettel with their purpose.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore






|







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

The following table lists all predefined zettel with their purpose.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
26
27
28
29
30
31
32

33
34
35
36
37




38
39
40


41
42
43
44
45
| [[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

| [[00000000019100]] | Zettelstore Sxn Code for Templates | Some helper 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]]
| [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file 
| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid




| [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]


| [[00010000000000]] | Home | Default home zettel, contains some welcome information

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

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







>
|


<

>
>
>
>
|
|

>
>





26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel
| [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel
| [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel
| [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text
| [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]]
| [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel
| [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message
| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates
| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]

| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid
| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]""
| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]""
| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]""
| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]""
| [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]
| [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]]
| [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]]
| [[00010000000000]] | Home | Default home zettel, contains some welcome information

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

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

Changes to docs/manual/00001006020100.zettel.

1
2
3
4
5

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









20
21
22
23
24
25
26
27
28
29
30
31
32
33
id: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

modified: 20220623183234

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
You are free to define your own roles.
It is allowed to set an empty value or to omit the role.

Some roles are defined for technical reasons:

; [!configuration|''configuration'']
: A zettel that contains some configuration data for the Zettelstore.
  Most prominent is [[00000000000100]], as described in [[00001004020000]].
; [!manual|''manual'']
: All zettel that document the inner workings of the Zettelstore software.
  This role is only used in this specific Zettelstore.










If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles:

; [!note|''note'']
: A small note, to remember something.
  Notes are not real zettel, they just help to create a real zettel.
  Think of them as Post-it notes.
; [!literature|''literature'']
: Contains some remarks about a book, a paper, a web page, etc.
  You should add a citation key for citing it.
; [!zettel|''zettel'']
: A real zettel that contains your own thoughts.

However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ...





>
|








|




>
>
>
>
>
>
>
>
>










|
|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
id: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231129173620

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
You are free to define your own roles.
It is allowed to set an empty value or to omit the role.

Some roles are defined for technical reasons:

; [!configuration|''configuration'']
: A zettel that contains some configuration data / information for the Zettelstore.
  Most prominent is [[00000000000100]], as described in [[00001004020000]].
; [!manual|''manual'']
: All zettel that document the inner workings of the Zettelstore software.
  This role is only used in this specific Zettelstore.
; [!role|''role'']
: A zettel with the role ""role"" and a title, which names a [[role|00001006020000#role]], is treated as a __role zettel__.
  Basically, role zettel describe the role, and form a hierarchiy of meta-roles.
; [!tag|''tag'']
: A zettel with the role ""tag"" and a title, which names a [[tag|00001006020000#tags]], is treated as a __tag zettel__.
  Basically, tag zettel describe the tag, and form a hierarchiy of meta-tags.
; [!zettel|''zettel'']
: A zettel that contains your own thoughts.
  The real reason to use this software.

If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles:

; [!note|''note'']
: A small note, to remember something.
  Notes are not real zettel, they just help to create a real zettel.
  Think of them as Post-it notes.
; [!literature|''literature'']
: Contains some remarks about a book, a paper, a web page, etc.
  You should add a citation key for citing it.
; ''zettel''
: (as described above)

However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ...

Changes to docs/manual/00001006034500.zettel.

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

24
25


26
27


28
29
30
31
32
33
34
35
id: 00001006034500
title: Timestamp Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230612183509

Values of this type denote a point in time.

=== Allowed values
Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"".

* YYYY is the year,
* MM is the month,
* DD is the day,
* hh is the hour,
* mm is the minute,
* ss is the second.

=== Query comparison
[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters.


When the [[search operators|00001007705000]] ""less"", ""not less"", ""greater"", and ""not greater"" are given, the length of the search value is checked.
If it contains less than 14 digits, zero digits (""0"") are appended, until it contains exactly 14 digits.



All other comparisons assume that up to 14 characters are given.



Comparison is done through the string representation.
In case of the search operators ""less"", ""not less"", ""greater"", and ""not greater"", this is the same as a numerical comparison.

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

If both values are timestamp values, this works well because both have the same length.






|




|








|
<

>
|
|
>
>

|
>
>





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

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


id: 00001006034500
title: Timestamp Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20231030182858

Values of this type denote a point in time.

=== Allowed values
Must be a sequence of 4, 6, 8, 10, 12, or 14 digits (""0""--""9"") (similar to an [[Identifier|00001006032000]]), with the restriction that it must conform to the pattern ""YYYYMMDDhhmmss"".

* YYYY is the year,
* MM is the month,
* DD is the day,
* hh is the hour,
* mm is the minute,
* ss is the second.

If the sequence is less than 14 digits, they are expanded with the following rule:


* YYYY is expanded to YYYY0101000000
* YYYYMM is expanded to YYYYMM01000000
* YYYYMMDD is expanded to YYYYMMDD000000
* YYYYMMDDhh is expanded to YYYYMMDDhh0000
* YYYYMMDDhhmm is expanded to YYYYMMDDhhmm00

=== Query comparison
[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters.
Then, they are treated as timestamp data, as describe above, if they contain 4, 6, 8, 10, or 12 digits.

Comparison is done through the string representation.
In case of the search operators ""less"", ""not less"", ""greater"", and ""not greater"", this is the same as a numerical comparison.

=== Sorting
Sorting is done by comparing the possibly expanded values.


Changes to docs/manual/00001007030900.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001007030900
title: Zettelmarkup: Comment Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218130330

Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted.
While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to JSON, the comment block will not be ignored but it will output some JSON text.
Same for other renderer.

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some percent sign characters in the text that should not be interpreted.






>
|








|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001007030900
title: Zettelmarkup: Comment Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170858

Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted.
While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to a symbolic expression, the comment block will not be ignored but it will output some text.
Same for other renderer.

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some percent sign characters in the text that should not be interpreted.

Changes to docs/manual/00001007031140.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20230116183656

A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''.
The line must literally start with the sequence ''{{{query:''.
Everything after this prefix is interpreted as a [[query expression|00001007700000]].

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20231023163751

A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''.
The line must literally start with the sequence ''{{{query:''.
Everything after this prefix is interpreted as a [[query expression|00001007700000]].

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.
45
46
47
48
49
50
51



52
53
54
55
56
57
58
59
60
61
62
63
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.



; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case[^Except if the key name collides with one of the above names. In this case use at least one lower case letter.].

Example:
```zmk
{{{query:tags:#search | tags}}}
```
This is a tag cloud of all tags that are used together with the tag #search:
:::example
{{{query:tags:#search | tags}}}
:::







>
>
>












45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REINDEX'' (aggregate)
: Will be ignored.
  This action may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but is here superfluous (and possibly harmful).
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case[^Except if the key name collides with one of the above names. In this case use at least one lower case letter.].

Example:
```zmk
{{{query:tags:#search | tags}}}
```
This is a tag cloud of all tags that are used together with the tag #search:
:::example
{{{query:tags:#search | tags}}}
:::

Changes to docs/manual/00001007040100.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007040100
title: Zettelmarkup: Text Formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131003

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

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





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007040100
title: Zettelmarkup: Text Formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231113191353

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

Text formatting can be nested, up to a reasonable limit.
25
26
27
28
29
30
31



32
33
* The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text.
** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}.
* The comma character (""'',''"", U+002C) produces sub-scripted text.
** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}.
* The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]].
** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}.
** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}.



* The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements.
** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi.







>
>
>


26
27
28
29
30
31
32
33
34
35
36
37
* The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text.
** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}.
* The comma character (""'',''"", U+002C) produces sub-scripted text.
** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}.
* The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]].
** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}.
** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}.
* The number sign (""''#''"", U+0023) marks the text visually, where the mark does not belong to the text itself.
  It is typically used to highlight some text that is important for you, but was not important for the original author.
** Example: ``abc ##def## ghi`` is rendered in HTML as: ::abc ##def## ghi::{=example}.
* The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements.
** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi.

Changes to docs/manual/00001007702000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007702000
title: Search term
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20230612180954

A search term allows you to specify one search restriction.
The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions.

A search term can be one of the following (the first three term are collectively called __search literals__):
* A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]].







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007702000
title: Search term
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20230925173539

A search term allows you to specify one search restriction.
The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions.

A search term can be one of the following (the first three term are collectively called __search literals__):
* A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]].

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  A zero value of N will produce the same result as if nothing was specified.
  If specified multiple times, the lower value takes precedence.

  Example: ''PICK 5 PICK 3'' will be interpreted as ''PICK 3''.
* The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list.
  If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed.

  Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSED published'' will return a reversed result order.

  An explicit order field will take precedence over the random order described below.

  If no random order is effective, a ``ORDER REVERSE id`` will be added.
  This makes the sort stable.

  Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE id``.







|







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  A zero value of N will produce the same result as if nothing was specified.
  If specified multiple times, the lower value takes precedence.

  Example: ''PICK 5 PICK 3'' will be interpreted as ''PICK 3''.
* The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list.
  If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed.

  Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSE published'' will return a reversed result order.

  An explicit order field will take precedence over the random order described below.

  If no random order is effective, a ``ORDER REVERSE id`` will be added.
  This makes the sort stable.

  Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE id``.

Changes to docs/manual/00001007721200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007721200
title: Query: Unlinked Directive
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211119133357
modified: 20230731163343

The value of a personal Zettelstore is determined in part by explicit connections between related zettel.
If the number of zettel grow, some of these connections are missing.
There are various reasons for this.
Maybe, you forgot that a zettel exists.
Or you add a zettel later, but forgot that previous zettel already mention its title.




|


|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007721200
title: Query: Unlinked Directive
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211119133357
modified: 20230928190540

The value of a personal Zettelstore is determined in part by explicit connections between related zettel.
If the number of zettel grow, some of these connections are missing.
There are various reasons for this.
Maybe, you forgot that a zettel exists.
Or you add a zettel later, but forgot that previous zettel already mention its title.

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
id: 00001007800000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk

modified: 20220810095559

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]] | [[Tag|00001007040000]]
| ''$''  | (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]]





>
|






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id: 00001007800000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
created: 20220805150154
modified: 20231113191330

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

|= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[marked / highlighted text|00001007040100]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]
| ''&''  | (free) | [[Entity|00001007040000]]
| ''\'''  | (free)  | [[Computer input|00001007040200]]
| ''(''  | (free) | (free)
| '')''  | (free) | (free)
| ''*''  | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]]

Changes to docs/manual/00001007903000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007903000
title: Zettelmarkup: First Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220810182917
modified: 20220926183359

[[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations.
Some characters have a special meaning, but you have to enter them is a defined way to see a visible change.
Zettelmarkup is designed to be used for zettel, which are relatively short.
It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example.

=== Paragraphs






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007903000
title: Zettelmarkup: First Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220810182917
modified: 20231201135849

[[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations.
Some characters have a special meaning, but you have to enter them is a defined way to see a visible change.
Zettelmarkup is designed to be used for zettel, which are relatively short.
It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example.

=== Paragraphs
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| ''An __emphasized__ word'' | An __emphasized__ word | Put two underscore characters before and after the text you want to emphasize
| ''Someone uses **bold** text'' | Someone uses **bold** text | Put two asterisks before and after the text you want to see bold
| ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote.

You probably see a principle.

One nice thing about the quotation mark characters: they are rendered according to the current language.
Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}, ""finnish""{lang=fi}.
You will see later, how to change the current language.

=== Lists
Quite often, text consists of lists.
Zettelmarkup supports different types of lists.
The most important lists are:
* Unnumbered lists,







|







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| ''An __emphasized__ word'' | An __emphasized__ word | Put two underscore characters before and after the text you want to emphasize
| ''Someone uses **bold** text'' | Someone uses **bold** text | Put two asterisks before and after the text you want to see bold
| ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote.

You probably see a principle.

One nice thing about the quotation mark characters: they are rendered according to the current language.
Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}.
You will see later, how to change the current language.

=== Lists
Quite often, text consists of lists.
Zettelmarkup supports different types of lists.
The most important lists are:
* Unnumbered lists,

Changes to docs/manual/00001007990000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001007990000
title: Zettelmarkup: Cheat Sheet
role: manual
tags: #manual #reference #zettelmarkup
syntax: zmk
created: 20221209191905
modified: 20221209193310

=== Overview
This Zettelmarkup cheat sheet provides a quick overview of many Zettelmarkup elements.
It can not cover any special case.
If you need more information about any of these elements, please refer to the detailed description.

=== Basic Syntax
|[[Text formatting|00001007040100]]|''__italic text__'' &rarr; __italic text__, ''**bold text**'' &rarr; **bold text**, ''""quoted text""'' &rarr; ""quoted text""
|[[Text editing|00001007040100]]|''>>inserted text>>'' &rarr; >>inserted text>>, ''~~deleted text~~'' &rarr; ~~deleted text~~
|[[Text literal formatting|00001007040200]]|''\'\'entered text\'\''' &rarr; ''entered text'', ''``source code``'' &rarr; ``source code``, ''==text output=='' &rarr; ==text output==
|[[Superscript, subscript|00001007040100]]|''m^^2^^'' &rarr; m^^2^^, ''H,,2,,O'' &rarr; H,,2,,O
|[[Links to other zettel|00001007040310]]|''[[Link text|00001007990000]]'' &rarr; [[Link text|00001007990000]]
|[[Links to external resources|00001007040310]]|''[[Zettelstore|https://zettelstore.de]]'' &rarr; [[Zettelstore|https://zettelstore.de]]
|[[Embed an image|00001007040322]]|''{{Image text|00000000040001}}'' &rarr; {{Image text|00000000040001}}
|[[Embed content of first paragraph|00001007040324]]|''{{00001007990000}}'' &rarr; {{00001007990000}}






|







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001007990000
title: Zettelmarkup: Cheat Sheet
role: manual
tags: #manual #reference #zettelmarkup
syntax: zmk
created: 20221209191905
modified: 20231201140000

=== Overview
This Zettelmarkup cheat sheet provides a quick overview of many Zettelmarkup elements.
It can not cover any special case.
If you need more information about any of these elements, please refer to the detailed description.

=== Basic Syntax
|[[Text formatting|00001007040100]]|''__italic text__'' &rarr; __italic text__, ''**bold text**'' &rarr; **bold text**, ''""quoted text""'' &rarr; ""quoted text"", ''##marked text##'' &rarr; ##marked text##
|[[Text editing|00001007040100]]|''>>inserted text>>'' &rarr; >>inserted text>>, ''~~deleted text~~'' &rarr; ~~deleted text~~
|[[Text literal formatting|00001007040200]]|''\'\'entered text\'\''' &rarr; ''entered text'', ''``source code``'' &rarr; ``source code``, ''==text output=='' &rarr; ==text output==
|[[Superscript, subscript|00001007040100]]|''m^^2^^'' &rarr; m^^2^^, ''H,,2,,O'' &rarr; H,,2,,O
|[[Links to other zettel|00001007040310]]|''[[Link text|00001007990000]]'' &rarr; [[Link text|00001007990000]]
|[[Links to external resources|00001007040310]]|''[[Zettelstore|https://zettelstore.de]]'' &rarr; [[Zettelstore|https://zettelstore.de]]
|[[Embed an image|00001007040322]]|''{{Image text|00000000040001}}'' &rarr; {{Image text|00000000040001}}
|[[Embed content of first paragraph|00001007040324]]|''{{00001007990000}}'' &rarr; {{00001007990000}}

Changes to docs/manual/00001012000000.zettel.

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


27
28
29
30
31
32
33
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230731162018

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

=== Background
The API is HTTP-based and uses plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software.

There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use.

=== Authentication
If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller.
* [[Authenticate an user|00001012050200]] to obtain an access token
* [[Renew an access token|00001012050400]] without costly re-authentication
* [[Provide an access token|00001012050600]] when doing an API call

=== Zettel lists
* [[List all zettel|00001012051200]]
* [[Query the list of all zettel|00001012051400]]



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






|






|












>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231128183617

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

=== Background
The API is HTTP-based and uses plain text and [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software.

There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use.

=== Authentication
If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller.
* [[Authenticate an user|00001012050200]] to obtain an access token
* [[Renew an access token|00001012050400]] without costly re-authentication
* [[Provide an access token|00001012050600]] when doing an API call

=== Zettel lists
* [[List all zettel|00001012051200]]
* [[Query the list of all zettel|00001012051400]]
* [[Determine a tag zettel|00001012051600]]
* [[Determine a role zettel|00001012051800]]

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

Changes to docs/manual/00001012051200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012051200
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230703180113

To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
Always use the endpoint ''/z'' to work with a list of zettel.

Without further specifications, a plain text document is returned, with one line per zettel.
Each line contains in the first 14 characters the [[zettel identifier|00001006050000]].
Separated by a space character, the title of the zettel follows:






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012051200
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170810

To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
Always use the endpoint ''/z'' to work with a list of zettel.

Without further specifications, a plain text document is returned, with one line per zettel.
Each line contains in the first 14 characters the [[zettel identifier|00001006050000]].
Separated by a space character, the title of the zettel follows:
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
* Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]].
* ''list'' starts a list of zettel.
* ''zettel'' itself start, well, a zettel.
* ''id'' denotes the zettel identifier, encoded as a string.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.

=== JSON output (deprecated)

Alternatively, you may retrieve the list of all zettel as a JSON object by specifying the encoding with the query parameter ''enc=json'':

```sh
# curl 'http://127.0.0.1:23123/z?enc=json'
{"query":"","human":"","list":[{"id":"00001012051200","meta":{"back":"00001012000000","backward":"00001012000000 00001012920000","box-number":"1","created":"20210126175322","forward":"00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000","modified":"20221219150626","published":"20221219150626","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: List all zettel"},"rights":62},{"id":"00001012050600","meta":{"back":"00001012000000 00001012080500","backward":"00001012000000 00001012080500","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012921000","modified":"20220218130020","published":"20220218130020","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Provide an access token"},"rights":62},{"id":"00001012050400","meta":{"back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20220107215751","published":"20220107215751","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"},"rights":62},{"id":"00001012050200","meta":{"back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20220107215844","published":"20220107215844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"},"rights":62}, ...]}
```

If you reformat the JSON output, you will see its structure better:
```
{
  "query": "",
  "human": "",
  "list": [
    {
      "id": "00001012051200",
      "meta": {
        "back": "00001012000000",
        "backward": "00001012000000 00001012920000",
        "box-number": "1",
        "created": "20210126175322",
        "forward": "00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000",
        "modified": "20221219151200",
        "published": "20221219151200",
        "role": "manual",
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
        "title": "API: List all zettel"
      },
      "rights": 62
    },
    {
      "id": "00001012050600",
      "meta": {
        "back": "00001012000000 00001012080500",
        "backward": "00001012000000 00001012080500",
        "box-number": "1",
        "created": "00010101000000",
        "forward": "00001012050200 00001012921000",
        "modified": "20220218130020",
        "published": "20220218130020",
        "role": "manual",
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
        "title": "API: Provide an access token"
      },
      "rights": 62
    },
    {
      "id": "00001012050400",
      "meta": {
        "back": "00001010040700 00001012000000",
        "backward": "00001010040700 00001012000000 00001012920000 00001012921000",
        "box-number": "1",
        "created": "00010101000000",
        "forward": "00001010040100 00001012050200 00001012920000 00001012921000",
        "modified": "20220107215751",
        "published": "20220107215751",
        "role": "manual",
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
        "title": "API: Renew an access token"
      },
      "rights": 62
    },
    {
      "id": "00001012050200",
      "meta": {
        "back": "00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012
080200",
        "backward": "00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 0000
1012053600 00001012080200 00001012920000 00001012921000",
        "box-number": "1",
        "created": "00010101000000",
        "forward": "00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000",
        "modified": "20220107215844",
        "published": "20220107215844",
        "role": "manual",
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
        "title": "API: Authenticate a client"
      },
      "rights": 62
    }
  ]
}
```

Again, the list is typically **not** sorted, for example by the zettel identifier.

The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects.
These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel).
The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings.

The JSON objects keys ''"query"'' and ''"human"'' will be explained later in this manual.

=== Note
This request (and similar others) will always return a list of metadata, provided the request was syntactically correct.
There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token.
In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty.

With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token).

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]].
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid.







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









|




61
62
63
64
65
66
67

































































































68
69
70
71
72
73
74
75
76
77
78
79
80
81
* Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]].
* ''list'' starts a list of zettel.
* ''zettel'' itself start, well, a zettel.
* ''id'' denotes the zettel identifier, encoded as a string.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.


































































































=== Note
This request (and similar others) will always return a list of metadata, provided the request was syntactically correct.
There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token.
In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty.

With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token).

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid.

Changes to docs/manual/00001012051400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20230731162234
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions.

A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below).
An empty search expression will select all zettel.
An empty list of action, or no valid action, returns the list of all selected zettel metadata.

Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''.

The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata.

It is allowed to specify this query parameter more than once.

This parameter loosely resembles the search form of the [[web user interface|00001014000000]] or those of [[Zettelmarkup's Query Transclusion|00001007031140]].

For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1'
00001012921000 API: JSON structure of an access token
00001012920500 Formats available by the API
00001012920000 Endpoints used by the API
...
```

If you want to retrieve a data document, as a [[symbolic expression|00001012930500]]:

```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=data'
(meta-list (query "title:API ORDER REVERSE id OFFSET 1") (human "title HAS API ORDER REVERSE id OFFSET 1") (list (zettel (id 1012921000) (meta (title "API: Structure of an access token") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012050600 00001012051200") (backward "00001012050200 00001012050400 00001012050600 00001012051200") (box-number "1") (created "20210126175322") (forward "00001012050200 00001012050400 00001012930000") (modified "20230412155303") (published "20230412155303")) (rights 62)) (zettel (id 1012920500) (meta (title "Encodings available via the API") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (backward "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (box-number "1") (created "20210126175322") (forward "00001012000000 00001012920510 00001012920513 00001012920516 00001012920519 00001012920522 00001012920525") (modified "20230403123653") (published "20230403123653")) (rights 62)) (zettel (id 1012920000) (meta (title "Endpoints used by the API") ...
```

The data object contains a key ''"meta-list"'' to signal that it contains a list of metadata values (and some more).
It contains the keys ''"query"'' and ''"human"'' with a string value.
Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]].
Without a selection, the values are the empty string.
''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans.
where its value is a list of zettel JSON objects.

The symbol ''list'' starts the list of zettel data.
Data of a zettel is indicated by the symbol ''zettel'', followed by ''(id ID)'' that describes the zettel identifier as a numeric value.
Leading zeroes are removed.
Metadata starts with the symbol ''meta'', and each metadatum itself is a list of metadata key / metadata value.
Metadata keys are encoded as a symbol, metadata values as a string.
''"rights"'' encodes the [[access rights|00001012921200]] for the given zettel.

If you want to retrieve a JSON document:

```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=json'
{"query":"title:API ORDER REVERSE id OFFSET 1","human":"title HAS API ORDER REVERSE id OFFSET 1","list":[{"id":"00001012921200","meta":{"back":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","backward":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","box-number":"1","created":"00010101000000","forward":"00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300","modified":"20220201171959","published":"20220201171959","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: Encoding of Zettel Access Rights"},"rights":62},{"id":"00001012921000","meta":{"back":"00001012050600 00001012051200","backward":"00001012050200 00001012050400 00001012050600 00001012051200","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012050400","published":"00010101000000","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: JSON structure of an access token"},"rights":62}, ...]
```

The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects.
These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel).
The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings.

Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value.

=== Aggregates

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







|



















|

















<








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







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

45
46
47
48
49
50
51
52













53
54
55
56
57
58
59
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20231023162927
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions.

A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below).
An empty search expression will select all zettel.
An empty list of action, or no valid action, returns the list of all selected zettel metadata.

Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''.

The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata.

It is allowed to specify this query parameter more than once.

This parameter loosely resembles the search form of the [[web user interface|00001014000000]] or those of [[Zettelmarkup's Query Transclusion|00001007031140]].

For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1'
00001012921000 API: Structure of an access token
00001012920500 Formats available by the API
00001012920000 Endpoints used by the API
...
```

If you want to retrieve a data document, as a [[symbolic expression|00001012930500]]:

```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=data'
(meta-list (query "title:API ORDER REVERSE id OFFSET 1") (human "title HAS API ORDER REVERSE id OFFSET 1") (list (zettel (id 1012921000) (meta (title "API: Structure of an access token") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012050600 00001012051200") (backward "00001012050200 00001012050400 00001012050600 00001012051200") (box-number "1") (created "20210126175322") (forward "00001012050200 00001012050400 00001012930000") (modified "20230412155303") (published "20230412155303")) (rights 62)) (zettel (id 1012920500) (meta (title "Encodings available via the API") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (backward "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (box-number "1") (created "20210126175322") (forward "00001012000000 00001012920510 00001012920513 00001012920516 00001012920519 00001012920522 00001012920525") (modified "20230403123653") (published "20230403123653")) (rights 62)) (zettel (id 1012920000) (meta (title "Endpoints used by the API") ...
```

The data object contains a key ''"meta-list"'' to signal that it contains a list of metadata values (and some more).
It contains the keys ''"query"'' and ''"human"'' with a string value.
Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]].
Without a selection, the values are the empty string.
''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans.


The symbol ''list'' starts the list of zettel data.
Data of a zettel is indicated by the symbol ''zettel'', followed by ''(id ID)'' that describes the zettel identifier as a numeric value.
Leading zeroes are removed.
Metadata starts with the symbol ''meta'', and each metadatum itself is a list of metadata key / metadata value.
Metadata keys are encoded as a symbol, metadata values as a string.
''"rights"'' encodes the [[access rights|00001012921200]] for the given zettel.














=== Aggregates

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

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146




147
148
149
150
151
152
153
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=data'
(aggregate "tags" (query "| tags") (human "| tags") (list ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ...
```

If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''.
You see from this that actions are separated by space characters.

Of course, this list can also be returned as a JSON object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|role&enc=json'
{"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}}
```

The JSON object only contains the key ''"map"'' with the value of another object.
This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value.

Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|tags''.
If successful, the output is a JSON object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=json'
{"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}
```

The JSON object only contains the key ''"map"'' with the value of another object.
This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value.

=== Actions

There are two types of actions: parameters and aggregates.
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.




; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case.

Only the first aggregate action will be executed.









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










>
>
>
>







95
96
97
98
99
100
101





















102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=data'
(aggregate "tags" (query "| tags") (human "| tags") (list ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ...
```

If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''.
You see from this that actions are separated by space characters.






















=== Actions

There are two types of actions: parameters and aggregates.
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''REINDEX'' (aggregate)
: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call.
  It is not really an aggregate, since it is used only for its side effect.
  It is allowed to specify another aggregate.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case.

Only the first aggregate action will be executed.


Added docs/manual/00001012051600.zettel.





































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
id: 00001012051600
title: API: Determine a tag zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20230928183339
modified: 20230929114937

The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""tag zettel"", i.e. a zettel that documents a given tag.

The query parameter ""''tag''"" allows you to specify a value that is interpreted as the name of a tag.
Zettelstore tries to determine the corresponding tag zettel.

A tag zettel is a zettel with the [[''role''|00001006020100]] value ""tag"" and a title that names the tag.
If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used.

For example, if you want to determine the tag zettel for the tag ""#api"", your request will be:
```sh
# curl -i 'http://127.0.0.1:23123/z?tag=%23api'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010
Content-Length: 14

00001019990010
```

Alternatively, you can omit the ''#'' character at the beginning of the tag:
```sh
# curl -i 'http://127.0.0.1:23123/z?tag=api'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010
Content-Length: 14

00001019990010
```

If there is a corresponding tag zettel, the response will use the HTTP status code 302 (""Found""), the HTTP response header ''Location'' will contain the URL of the tag zettel.
Its zettel identifier will be returned in the HTTP response body.

If you specified some more query parameter, these will be part of the URL in the response header ''Location'':

```sh
# curl -i 'http://127.0.0.1:23123/z?tag=%23api&part=zettel'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010?part=zettel
Content-Length: 14

00001019990010
```

Otherwise, if no tag zettel was found, the response will use the HTTP status code 404 (""Not found"").

```sh
# curl -i 'http://127.0.0.1:23123/z?tag=notag'
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Content-Length: 29

Tag zettel not found: #notag
```

To fulfill this service, Zettelstore will evaluate internally the query ''role:tag title=TAG'', there ''TAG'' is the actual tag.

Of course, if you are interested in the URL of the tag zettel, you can make use of the HTTP ''HEAD'' method:

```sh
# curl -I 'http://127.0.0.1:23123/z?tag=%23api'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010
Content-Length: 14
```

=== HTTP Status codes
; ''302''
: Tag zettel was found.
  The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier.
; ''404''
: No zettel for the given tag was found.

Added docs/manual/00001012051800.zettel.















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
id: 00001012051800
title: API: Determine a role zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20231128183917
modified: 20231128184701

The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""role zettel"", i.e. a zettel that documents a given role.

The query parameter ""''role''"" allows you to specify a value that is interpreted as the name of a role.
Zettelstore tries to determine the corresponding role zettel.

A role zettel is a zettel with the [[''role''|00001006020100]] value ""role"" and a title that names the role.
If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used.

For example, if you want to determine the role zettel for the role ""manual"", your request will be:
```sh
# curl -i 'http://127.0.0.1:23123/z?role=manual'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/20231128184200
Content-Length: 14

20231128184200
```

If there is a corresponding role zettel, the response will use the HTTP status code 302 (""Found""), the HTTP response header ''Location'' will contain the URL of the role zettel.
Its zettel identifier will be returned in the HTTP response body.

If you specified some more query parameter, these will be part of the URL in the response header ''Location'':

```sh
# curl -i 'http://127.0.0.1:23123/z?role=manual&part=zettel'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/20231128184200?part=zettel
Content-Length: 14

20231128184200
```

Otherwise, if no role zettel was found, the response will use the HTTP status code 404 (""Not found"").

```sh
# curl -i 'http://127.0.0.1:23123/z?role=norole'
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Content-Length: 30

Role zettel not found: norole
```

To fulfill this service, Zettelstore will evaluate internally the query ''role:role title=ROLE'', there ''ROLE'' is the actual role.

Of course, if you are only interested in the URL of the role zettel, you can make use of the HTTP ''HEAD'' method:

```sh
# curl -I 'http://127.0.0.1:23123/z?role=manual'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/20231128184200
Content-Length: 14
```

=== HTTP Status codes
; ''302''
: Role zettel was found.
  The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier.
; ''404''
: No zettel for the given role was found.

Changes to docs/manual/00001012053200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20230807124310

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


The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20230807170416

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


The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
=== Data input
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.

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

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

Therefore, a body containing just ''{}'' is perfectly valid.
The new zettel will have no content, and only an identifier as [[metadata|00001006020000]]:

```
# curl -X POST --data '{}' 'http://127.0.0.1:23123/z?enc=json'
{"id":"20210713161000"}
```
If creating the zettel was successful, the HTTP response will contain a JSON object with one key:
; ''"id"''
: Contains the [[zettel identifier|00001006050000]] of the created zettel for further usage.

As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing:
```
# curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' 'http://127.0.0.1:23123/z?enc=json'
{"id":"20210713163100"}
```

=== HTTP Status codes
; ''201''
: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (JSON object or plain text).
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, the JSON was not formed according to above rules.
; ''403''
: You are not allowed to create a new zettel.







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


|



|


25
26
27
28
29
30
31





































32
33
34
35
36
37
38
39
40
=== Data input
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.






































=== HTTP Status codes
; ''201''
: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (data value or plain text).
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, the symbolic expression was not formed according to above rules.
; ''403''
: You are not allowed to create a new zettel.

Changes to docs/manual/00001012053300.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20230807123533

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

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

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20230807170259

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

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

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.
* ''"encoding"'' states how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
* The zettel contents is stored as a value of the key ''content''.
  Typically, text content is not encoded, and binary content is encoded via Base64.

=== JSON output (deprecated)

You may also retrieve the zettel as a JSON object by providing the query parameter ''enc=json'':
```sh
# curl 'http://127.0.0.1:23123/z/00001012053300?enc=json&part=zettel'
{"id":"00001012053300","meta":{"back":"00001012000000 00001012054400","backward":"00001012000000 00001012054400 00001012920000","box-number":"1","created":"20211004093206","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200","modified":"20221219160211","published":"20221219160211","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata and content of an existing zettel"},"encoding":"","content":"The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
```

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

The following keys of the JSON object are used:
; ''"id"''
: The zettel identifier of the zettel you requested.
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
; ''"encoding"''
: States how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
; ''"content"''
: Is a string value that contains the content of the zettel to be created.
  Typically, text content is not encoded, and binary content is encoded via Base64.
; ''"rights"''
: An integer number that describes the [[access rights|00001012921200]] for the zettel.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object / plain zettel data.
; ''204''
: Request was valid, but there is no data to be returned.
  Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.







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


|












70
71
72
73
74
75
76















































77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.
* ''"encoding"'' states how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
* The zettel contents is stored as a value of the key ''content''.
  Typically, text content is not encoded, and binary content is encoded via Base64.
















































=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate data value.
; ''204''
: Request was valid, but there is no data to be returned.
  Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012053400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230703175153

The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

To retrieve the plain metadata of a zettel, use the query parameter ''part=meta''

````sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta'






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230807170155

The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

To retrieve the plain metadata of a zettel, use the query parameter ''part=meta''

````sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta'
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
      (rights 62))
```

* The result is a list, starting with the symbol ''list''.
* Then, some key/value pairs are following, also nested.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.

=== JSON output (deprecated)
To return a JSON object, use also the query parameter ''enc=json''.

```sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta&enc=json'
{"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300","box-number":"1","created":"20210726174524","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200","modified":"20230703174515","published":"20230703174515","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62}
```

Pretty-printed, this results in:
```
{
  "meta": {
    "back": "00001012000000 00001012053300",
    "backward": "00001012000000 00001012053300 00001012920000",
    "box-number": "1",
    "created": "20210726174524",
    "forward": "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200",
    "modified": "20220917175233",
    "published": "20220917175233",
    "role": "manual",
    "syntax": "zmk",
    "tags": "#api #manual #zettelstore",
    "title": "API: Retrieve metadata of an existing zettel"
  },
  "rights": 62
}
```
The following keys of the JSON object are used:
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
; ''"rights"''
: An integer number that describes the [[access rights|00001012921200]] for the zettel.


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








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


|









43
44
45
46
47
48
49
50




































51
52
53
54
55
56
57
58
59
60
61
62
      (rights 62))
```

* The result is a list, starting with the symbol ''list''.
* Then, some key/value pairs are following, also nested.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.





































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

Changes to docs/manual/00001012053500.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230109105303

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

For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''.
If successful, the output is a JSON object:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) (TEXT "evaluated") (SPACE) (TEXT "metadata") (SPACE) (TEXT "and") (SPACE) (TEXT "content") (SPACE) (TEXT "of") (SPACE) (TEXT "a") (SPACE) (TEXT "specific") (SPACE) (TEXT "zettel") (SPACE) (TEXT "is") (SPACE) (LITERAL-INPUT () "/z/{ID}") (TEXT ",") (SPACE) (TEXT "where") (SPACE) (LITERAL-INPUT () "{ID}") ...
```

To select another encoding, you must provide the query parameter ''enc=ENCODING''.
Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]].






|




|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230807170112

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

For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''.
If successful, the output is a symbolic expression value:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) (TEXT "evaluated") (SPACE) (TEXT "metadata") (SPACE) (TEXT "and") (SPACE) (TEXT "content") (SPACE) (TEXT "of") (SPACE) (TEXT "a") (SPACE) (TEXT "specific") (SPACE) (TEXT "zettel") (SPACE) (TEXT "is") (SPACE) (LITERAL-INPUT () "/z/{ID}") (TEXT ",") (SPACE) (TEXT "where") (SPACE) (LITERAL-INPUT () "{ID}") ...
```

To select another encoding, you must provide the query parameter ''enc=ENCODING''.
Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]].
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<h1>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</h1>
<p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/z/{ID}</kbd>,
...
```

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







|









44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<h1>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</h1>
<p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/z/{ID}</kbd>,
...
```

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

Changes to docs/manual/00001012053600.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
id: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109105034

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

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

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.
The same default values applies to this endpoint.

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






|


















|









1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
id: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170019

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

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

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.
The same default values applies to this endpoint.

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

Changes to docs/manual/00001012054200.zettel.

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

Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]].
In both cases you must provide the data for the new or updated zettel in the body of the HTTP request.

One difference is the endpoint.
The [[endpoint|00001012920000]] to update a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
You must send a HTTP PUT request to that endpoint.

The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].

```
# curl -X POST --data 'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200
```

=== Data input
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.

=== JSON input (deprecated)
Alternatively, you can use the JSON encoding by using the query parameter ''enc=json''.

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

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

=== HTTP Status codes
; ''204''
: Update was successful, there is no body in the response.
; ''400''
: Request was not valid.
  For example, the request body was not valid.
; ''403''
: You are not allowed to delete the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.






|












|









<
<
<
<
<
<
<
<
<
<
<











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











30
31
32
33
34
35
36
37
38
39
40
id: 00001012054200
title: API: Update a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20231116110417

Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]].
In both cases you must provide the data for the new or updated zettel in the body of the HTTP request.

One difference is the endpoint.
The [[endpoint|00001012920000]] to update a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
You must send a HTTP PUT request to that endpoint.

The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].

```
# curl -X PUT --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200
```

=== Data input
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.












=== HTTP Status codes
; ''204''
: Update was successful, there is no body in the response.
; ''400''
: Request was not valid.
  For example, the request body was not valid.
; ''403''
: You are not allowed to delete the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012070500.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012070500
title: Retrieve administrative data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 00010101000000
modified: 20230701160903

The [[endpoint|00001012920000]] ''/x'' allows you to retrieve some (administrative) data.

Currently, you can only request Zettelstore version data.

````
# curl 'http://127.0.0.1:23123/x'

|



|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012070500
title: API: Retrieve administrative data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220304164242
modified: 20230928190516

The [[endpoint|00001012920000]] ''/x'' allows you to retrieve some (administrative) data.

Currently, you can only request Zettelstore version data.

````
# curl 'http://127.0.0.1:23123/x'

Changes to docs/manual/00001012921000.zettel.

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


15






id: 00001012921000
title: API: Structure of an access token
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230412155303

If the [[authentication process|00001012050200]] was successful, an access token with some additional data is returned.
The same is true, if the access token was [[renewed|00001012050400]].
The response is structured as a [[symbolic expression|00001012930000]] list, with the following elements:

# The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]]
# The token itself, as string value, which is technically a [[JSON Web Token|https://tools.ietf.org/html/rfc7519]] (JWT, RFC 7915)


# An integer that gives a hint about the lifetime / endurance of the token, measured in seconds












|






|
>
>
|
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001012921000
title: API: Structure of an access token
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807165915

If the [[authentication process|00001012050200]] was successful, an access token with some additional data is returned.
The same is true, if the access token was [[renewed|00001012050400]].
The response is structured as a [[symbolic expression|00001012930000]] list, with the following elements:

# The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]]
# The token itself, which is technically the string representation of a [[symbolic expression|00001012930500]] containing relevant data, plus a check sum.
#* The symbolic expression has the form ''(KIND USERNAME NOW EXPIRE Z-ID)''
#* ''KIND'' is ''0'' for an API access, ''1'' if it created for the Web user interface.
#* ''USERNAME'' is the user name of the user.
#* ''NOW'' is a timestamp of the current time.
#* ''EXPIRE'' is the timestamp when the access token expires.
#* ''Z-ID'' is the zettel identifier of the user zettel.
  The symbolic expression is encoded via ""base64"".
  Based on this encoding, a checksum is calculated, also encoded via ""base64"".
  Both encoded values are concatenated, with a period (''"."'') as a delimiter.

Changes to docs/manual/00001012921200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
id: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

modified: 20220201171959

Various API calls return a JSON key ''"rights"'' that encodes the access rights the user currently has.
It 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.





>
|

|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
id: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220201173115
modified: 20230807164817

Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has.
''N'' is an integer number between 0 and 62.[^Not all values in this range are used.]

The value ""0"" signals that something went wrong internally while determining the access rights.

A value of ""1"" says, that the current user has no access right for the given zettel.
In most cases, this value will not occur, because only zettel are presented, which are at least readable by the current user.

Values ""2"" to ""62"" are binary encoded values, where each bit signals a special right.

Changes to docs/manual/00001012931600.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20230405123222

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, typically a word.







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20231113191203

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, typically a word.

143
144
145
146
147
148
149





150
151
152
153
154
155
156
The inline text should be treated as emphasized.

:::syntax
__InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as inserted.






:::syntax
__QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as quoted text.

:::syntax
__SpanFormat__ **=** ''(FORMAT-SPAN'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.







>
>
>
>
>







143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
The inline text should be treated as emphasized.

:::syntax
__InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as inserted.

:::syntax
__MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as highlighted for the reader (but was not important fto the original author).

:::syntax
__QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as quoted text.

:::syntax
__SpanFormat__ **=** ''(FORMAT-SPAN'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.

Changes to docs/manual/00001017000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001017000000
title: Tips and Tricks
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20220803170112
modified: 20221205154406

=== Welcome Zettel
* **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users.
  In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel.
  Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]].
* **Solution 1:**
*# Create a new zettel with all your references to internal, non-public zettel.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001017000000
title: Tips and Tricks
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20220803170112
modified: 20231012154803

=== Welcome Zettel
* **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users.
  In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel.
  Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]].
* **Solution 1:**
*# Create a new zettel with all your references to internal, non-public zettel.
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel.

=== Role-specific Layout of Zettel in Web User Interface (WebUI)
[!role-css]
* **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]].
  For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel.
  Or you want a multi-column layout.
* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Role to CSS Map|00000000029000]]"" (its identifier is ''00000000029000'').
  This zettel maps a role name to a zettel that must contain the role-specific CSS code.

  First, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must).
  Set its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"".
  The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color.

  Let's assume, the newly created CSS zettel got the identifier ''20220825200100''.

  Now, you have to connect this zettel to the zettel called ""Zettelstore Role CSS Map"".
  Since you have enabled ''expert-mode'', you are allowed to modify it.
  Add the following metadata ''css-configuration-zid: 20220825200100'' to assign the role-specific CSS code for the role ""configuration"" to the CSS zettel containing that CSS.

  In general, its role-assigning metadata must be like this pattern: ''css-ROLE-zid: ID'', where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code.
  It is allowed to assign more than one role to a specific CSS zettel.
* **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role.
  For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata.
* **Extension:** if you have already established a role-specific layout for zettel, but you additionally want just one another zettel with another role to be rendered with the same CSS, you have to add metadata to the one zettel: ''css-role: ROLE'', where ''ROLE'' is the placeholder for the role that already is assigned to a specific CSS-based layout.

=== Zettel synchronization with iCloud (Apple)
* **Problem:** You use Zettelstore on various macOS computers and you want to use the sameset of zettel across all computers.
* **Solution:** Place your zettel in an iCloud folder.

  To configure Zettelstore to use the folder, you must specify its location within you directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number).
  Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''.







|
|

|
|




|
|
|

|
|


<







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

49
50
51
52
53
54
55
  If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel.

=== Role-specific Layout of Zettel in Web User Interface (WebUI)
[!role-css]
* **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]].
  For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel.
  Or you want a multi-column layout.
* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Sxn Start Code|00000000019000]]"" (its identifier is ''00000000019000'').
  This zettel is the starting point for Sxn code, where you can place a definition for a variable named ""CSS-ROLE-map"".

  But first, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must).
  Its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"".
  The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color.

  Let's assume, the newly created CSS zettel got the identifier ''20220825200100''.

  Now, you have to map this freshly created zettel to a role, for example ""zettel"".
  Since you have enabled ''expert-mode'', you are allowed to modify the zettel ""[[Zettelstore Sxn Start Code|00000000019000]]"".
  Add the following code to the Sxn Start Code zettel: ``(set! CSS-ROLE-map '(("zettel" . "20220825200100")))``.

  In general, the mapping must follow the pattern: ``(ROLE . ID)``, where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code.
  For example, if you also want the role ""configuration"" to be rendered using that CSS, the code should be something like ``(set! CSS-ROLE-map '(("zettel" . "20220825200100") ("configuration" . "20220825200100")))``.
* **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role.
  For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata.


=== Zettel synchronization with iCloud (Apple)
* **Problem:** You use Zettelstore on various macOS computers and you want to use the sameset of zettel across all computers.
* **Solution:** Place your zettel in an iCloud folder.

  To configure Zettelstore to use the folder, you must specify its location within you directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number).
  Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''.

Added docs/manual/00001019990010.zettel.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
id: 00001019990010
title: #api
role: tag
syntax: zmk
created: 20230928185004
modified: 20230928185204

Zettel with the tag ''#api'' contain a description of the [[API|00001012000000]].

Added docs/manual/20231128184200.zettel.















>
>
>
>
>
>
>
1
2
3
4
5
6
7
id: 20231128184200
title: manual
role: role
syntax: zmk
created: 20231128184200

Zettel with the role ""manual"" contain the manual of the zettelstore.

Changes to encoder/encoder.go.

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





57
58
59
60
61
62
63
	ErrNoWriteMeta    = errors.New("method WriteMeta is not implemented")
	ErrNoWriteContent = errors.New("method WriteContent is not implemented")
	ErrNoWriteBlocks  = errors.New("method WriteBlocks is not implemented")
	ErrNoWriteInlines = errors.New("method WriteInlines is not implemented")
)

// Create builds a new encoder with the given options.
func Create(enc api.EncodingEnum) Encoder {
	if create, ok := registry[enc]; ok {
		return create()
	}
	return nil
}

// CreateFunc produces a new encoder.
type CreateFunc func() Encoder






var registry = map[api.EncodingEnum]CreateFunc{}

// Register the encoder for later retrieval.
func Register(enc api.EncodingEnum, create CreateFunc) {
	if _, ok := registry[enc]; ok {
		panic(fmt.Sprintf("Encoder %q already registered", enc))







|

|





|
>
>
>
>
>







41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
	ErrNoWriteMeta    = errors.New("method WriteMeta is not implemented")
	ErrNoWriteContent = errors.New("method WriteContent is not implemented")
	ErrNoWriteBlocks  = errors.New("method WriteBlocks is not implemented")
	ErrNoWriteInlines = errors.New("method WriteInlines is not implemented")
)

// Create builds a new encoder with the given options.
func Create(enc api.EncodingEnum, params *CreateParameter) Encoder {
	if create, ok := registry[enc]; ok {
		return create(params)
	}
	return nil
}

// CreateFunc produces a new encoder.
type CreateFunc func(*CreateParameter) Encoder

// CreateParameter contains values that are needed to create an encoder.
type CreateParameter struct {
	Lang string // default language
}

var registry = map[api.EncodingEnum]CreateFunc{}

// Register the encoder for later retrieval.
func Register(enc api.EncodingEnum, create CreateFunc) {
	if _, ok := registry[enc]; ok {
		panic(fmt.Sprintf("Encoder %q already registered", enc))

Changes to encoder/encoder_block_test.go.

295
296
297
298
299
300
301












302
303
304
305
306
307
308
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},












	{
		descr: "Simple Table",
		zmk:   "|c1|c2|c3\n|d1||d3",
		expect: expectMap{
			encoderHTML:  `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TABLE () (list (CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) (list (CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,







>
>
>
>
>
>
>
>
>
>
>
>







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
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with keys, but no descriptions",
		zmk:   "; K1\n: D11\n: D12\n; K2\n; K3\n: D31",
		expect: expectMap{
			encoderHTML:  "<dl><dt>K1</dt><dd><p>D11</p></dd><dd><p>D12</p></dd><dt>K2</dt><dt>K3</dt><dd><p>D31</p></dd></dl>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION (INLINE (TEXT "K1")) (BLOCK (BLOCK (PARA (TEXT "D11"))) (BLOCK (PARA (TEXT "D12")))) (INLINE (TEXT "K2")) (BLOCK) (INLINE (TEXT "K3")) (BLOCK (BLOCK (PARA (TEXT "D31"))))))`,
			encoderSHTML: `((dl (dt "K1") (dd (p "D11")) (dd (p "D12")) (dt "K2") (dt "K3") (dd (p "D31"))))`,
			encoderText:  "K1\nD11\nD12\nK2\nK3\nD31",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Table",
		zmk:   "|c1|c2|c3\n|d1||d3",
		expect: expectMap{
			encoderHTML:  `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TABLE () (list (CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) (list (CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,

Changes to encoder/encoder_inline_test.go.

143
144
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
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderHTML:  "<q>quotes</q>",
			encoderMD:    "<q>quotes</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSHTML: `((q "quotes"))`,
			encoderText:  `quotes`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderHTML:  `<span lang="de"><q>quotes</q></span>`,
			encoderMD:    "<q>quotes</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE (quote (("lang" . "de"))) (TEXT "quotes")))`,
			encoderSHTML: `((span (@ (lang . "de")) (q "quotes")))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},




























































	{
		descr: "Span formatting",
		zmk:   `::span::`,
		expect: expectMap{
			encoderHTML:  `<span>span</span>`,
			encoderMD:    "span",
			encoderSz:    `(INLINE (FORMAT-SPAN () (TEXT "span")))`,







|


|








|


|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







143
144
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
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderHTML:  "&ldquo;quotes&rdquo;",
			encoderMD:    "<q>quotes</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "quotes" (@H "&rdquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderHTML:  `<span lang="de">&bdquo;quotes&ldquo;</span>`,
			encoderMD:    "<q>quotes</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE (quote (("lang" . "de"))) (TEXT "quotes")))`,
			encoderSHTML: `((span (@ (lang . "de")) (@H "&bdquo;") "quotes" (@H "&ldquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Empty quotes (default)",
		zmk:   `""""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;&rdquo;`,
			encoderMD:    "<q></q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE ()))`,
			encoderSHTML: `((@L (@H "&ldquo;" "&rdquo;")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Empty quotes (unknown)",
		zmk:   `""""{lang=unknown}`,
		expect: expectMap{
			encoderHTML:  `<span lang="unknown">&quot;&quot;</span>`,
			encoderMD:    "<q></q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE (quote (("lang" . "unknown")))))`,
			encoderSHTML: `((span (@ (lang . "unknown")) (@H "&quot;" "&quot;")))`,
			encoderText:  ``,
			encoderZmk:   `""""{lang="unknown"}`,
		},
	},
	{
		descr: "Nested quotes (default)",
		zmk:   `""say: ::""yes, ::""or?""::""::""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;say: <span>&lsquo;yes, <span>&ldquo;or?&rdquo;</span>&rsquo;</span>&rdquo;`,
			encoderMD:    "<q>say: <q>yes, <q>or?</q></q></q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "say:") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes,") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?")))))))`,
			encoderSHTML: `((@L (@H "&ldquo;") "say:" " " (span (@L (@H "&lsquo;") "yes," " " (span (@L (@H "&ldquo;") "or?" (@H "&rdquo;"))) (@H "&rsquo;"))) (@H "&rdquo;")))`,
			encoderText:  `say: yes, or?`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Two quotes",
		zmk:   `""yes"" or ""no""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;yes&rdquo; or &ldquo;no&rdquo;`,
			encoderMD:    "<q>yes</q> or <q>no</q>",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (SPACE) (TEXT "or") (SPACE) (FORMAT-QUOTE () (TEXT "no")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "yes" (@H "&rdquo;")) " " "or" " " (@L (@H "&ldquo;") "no" (@H "&rdquo;")))`,
			encoderText:  `yes or no`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark formatting",
		zmk:   `##marked##`,
		expect: expectMap{
			encoderHTML:  `<mark>marked</mark>`,
			encoderMD:    "<mark>marked</mark>",
			encoderSz:    `(INLINE (FORMAT-MARK () (TEXT "marked")))`,
			encoderSHTML: `((mark "marked"))`,
			encoderText:  `marked`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Span formatting",
		zmk:   `::span::`,
		expect: expectMap{
			encoderHTML:  `<span>span</span>`,
			encoderMD:    "span",
			encoderSz:    `(INLINE (FORMAT-SPAN () (TEXT "span")))`,
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderHTML:  `<span lang="fr"><q>abc</q></span>`,
			encoderMD:    "<q>abc</q>",
			encoderSz:    `(INLINE (FORMAT-SPAN (quote (("lang" . "fr"))) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSHTML: `((span (@ (lang . "fr")) (q "abc")))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
		},
	},
	{
		descr: "Simple Citation",
		zmk:   `[@Stern18]`,







|


|







311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderHTML:  `<span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span>`,
			encoderMD:    "<q>abc</q>",
			encoderSz:    `(INLINE (FORMAT-SPAN (quote (("lang" . "fr"))) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSHTML: `((span (@ (lang . "fr")) (@L (@H "&laquo;" "&nbsp;") "abc" (@H "&nbsp;" "&raquo;"))))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
		},
	},
	{
		descr: "Simple Citation",
		zmk:   `[@Stern18]`,

Changes to encoder/encoder_test.go.

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk)
		checkSz(t, testNum, pe, tc.descr)
	}
}

func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) {
	for enc, exp := range expected {
		encdr := encoder.Create(enc)
		got, err := pe.encode(encdr)
		if err != nil {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + pe.mode()







|







74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk)
		checkSz(t, testNum, pe, tc.descr)
	}
}

func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) {
	for enc, exp := range expected {
		encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		got, err := pe.encode(encdr)
		if err != nil {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + pe.mode()
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}

func checkSz(t *testing.T, testNum int, pe parserEncoder, descr string) {
	t.Helper()
	encdr := encoder.Create(encoderSz)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxreader.MakeReader(strings.NewReader(exp)).Read()
	if err != nil {







|







101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}

func checkSz(t *testing.T, testNum int, pe parserEncoder, descr string) {
	t.Helper()
	encdr := encoder.Create(encoderSz, nil)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxreader.MakeReader(strings.NewReader(exp)).Read()
	if err != nil {

Changes to encoder/htmlenc/htmlenc.go.

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

41
42
43
44
45
46
47

48
49
50
51
52

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	"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() encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *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.NewTransformer(1, nil),

		textEnc: textenc.Create(),
	}
}

type Encoder struct {
	tx      *szenc.Transformer
	th      *shtml.Transformer

	textEnc *textenc.Encoder
}

// WriteZettel encodes a full zettel as HTML5.
func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {

	hm, err := he.th.Transform(he.tx.GetMeta(zn.InhMeta, evalMeta))
	if err != nil {
		return 0, err
	}

	var isTitle ast.InlineSlice
	var htitle *sx.Pair
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		isTitle = parser.ParseSpacedText(plainTitle)
		xtitle := he.tx.GetSz(&isTitle)
		htitle, err = he.th.Transform(xtitle)
		if err != nil {
			return 0, err
		}
	}

	xast := he.tx.GetSz(&zn.Ast)
	hast, err := he.th.Transform(xast)
	if err != nil {
		return 0, err
	}
	hen := he.th.Endnotes()

	sf := he.th.SymbolFactory()
	symAttr := sf.MustMake(sxhtml.NameSymAttr)

	head := sx.MakeList(sf.MustMake("head"))
	curr := head
	curr = curr.AppendBang(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sf.MustMake("charset"), sx.MakeString("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta")))
	for elem := hm; elem != nil; elem = elem.Tail() {
		curr = curr.AppendBang(elem.Car())
	}
	var sb strings.Builder
	if hasTitle {
		he.textEnc.WriteInlines(&sb, &isTitle)
	} else {
		sb.Write(zn.Meta.Zid.Bytes())
	}
	_ = curr.AppendBang(sx.Nil().Cons(sx.MakeString(sb.String())).Cons(sf.MustMake("title")))

	body := sx.MakeList(sf.MustMake("body"))
	curr = body
	if hasTitle {
		curr = curr.AppendBang(htitle.Cons(sf.MustMake("h1")))
	}
	for elem := hast; elem != nil; elem = elem.Tail() {







|



|




|
>






|
>





>
|










|






|



|






|









|







24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) })
}

// Create an encoder.
func Create(params *encoder.CreateParameter) *Encoder {
	// We need a new transformer every time, because tx.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{
		tx:      szenc.NewTransformer(),
		th:      shtml.NewEvaluator(1, nil),
		lang:    params.Lang,
		textEnc: textenc.Create(),
	}
}

type Encoder struct {
	tx      *szenc.Transformer
	th      *shtml.Evaluator
	lang    string
	textEnc *textenc.Encoder
}

// WriteZettel encodes a full zettel as HTML5.
func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(zn.InhMeta, evalMeta), &env)
	if err != nil {
		return 0, err
	}

	var isTitle ast.InlineSlice
	var htitle *sx.Pair
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		isTitle = parser.ParseSpacedText(plainTitle)
		xtitle := he.tx.GetSz(&isTitle)
		htitle, err = he.th.Evaluate(xtitle, &env)
		if err != nil {
			return 0, err
		}
	}

	xast := he.tx.GetSz(&zn.Ast)
	hast, err := he.th.Evaluate(xast, &env)
	if err != nil {
		return 0, err
	}
	hen := he.th.Endnotes(&env)

	sf := he.th.SymbolFactory()
	symAttr := sf.MustMake(sxhtml.NameSymAttr)

	head := sx.MakeList(sf.MustMake("head"))
	curr := head
	curr = curr.AppendBang(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sf.MustMake("charset"), sx.String("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta")))
	for elem := hm; elem != nil; elem = elem.Tail() {
		curr = curr.AppendBang(elem.Car())
	}
	var sb strings.Builder
	if hasTitle {
		he.textEnc.WriteInlines(&sb, &isTitle)
	} else {
		sb.Write(zn.Meta.Zid.Bytes())
	}
	_ = curr.AppendBang(sx.Nil().Cons(sx.String(sb.String())).Cons(sf.MustMake("title")))

	body := sx.MakeList(sf.MustMake("body"))
	curr = body
	if hasTitle {
		curr = curr.AppendBang(htitle.Cons(sf.MustMake("h1")))
	}
	for elem := hast; elem != nil; elem = elem.Tail() {
111
112
113
114
115
116
117

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

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

149
150
151
152
153
154
155
156
157
158
159

	gen := sxhtml.NewGenerator(sf, sxhtml.WithNewline)
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {

	hm, err := he.th.Transform(he.tx.GetMeta(m, evalMeta))
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator(he.th.SymbolFactory(), sxhtml.WithNewline)
	return gen.WriteListHTML(w, hm)
}

func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks encodes a block slice.
func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {

	hobj, err := he.th.Transform(he.tx.GetSz(bs))
	if err == nil {
		gen := sxhtml.NewGenerator(he.th.SymbolFactory())
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, he.th.Endnotes())
		length += l
		return length, err2
	}
	return 0, err
}

// WriteInlines writes an inline slice to the writer
func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {

	hobj, err := he.th.Transform(he.tx.GetSz(is))
	if err == nil {
		gen := sxhtml.NewGenerator(sx.FindSymbolFactory(hobj))
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}
		return length, nil
	}
	return 0, err
}







>
|













>
|







|








>
|










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

	gen := sxhtml.NewGenerator(sf, sxhtml.WithNewline)
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator(he.th.SymbolFactory(), sxhtml.WithNewline)
	return gen.WriteListHTML(w, hm)
}

func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks encodes a block slice.
func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env)
	if err == nil {
		gen := sxhtml.NewGenerator(he.th.SymbolFactory())
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, he.th.Endnotes(&env))
		length += l
		return length, err2
	}
	return 0, err
}

// WriteInlines writes an inline slice to the writer
func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(is), &env)
	if err == nil {
		gen := sxhtml.NewGenerator(sx.FindSymbolFactory(hobj))
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}
		return length, nil
	}
	return 0, err
}

Changes to encoder/mdenc/mdenc.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderMD, func() encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myME }

type Encoder struct{}








|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderMD, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myME }

type Encoder struct{}

327
328
329
330
331
332
333




334
335
336
337
338
339
340
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.b.WriteString("<q>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</q>")




	default:
		ast.Walk(v, &fn.Inlines)
	}
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {







>
>
>
>







327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.b.WriteString("<q>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</q>")
	case ast.FormatMark:
		v.b.WriteString("<mark>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</mark>")
	default:
		ast.Walk(v, &fn.Inlines)
	}
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {

Changes to encoder/shtmlenc/shtmlenc.go.

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

37
38
39
40
41
42

43
44
45
46

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

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

74
75
76
77
78
79
80
81
82

83
84
85
86
87
88
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSHTML, func() encoder.Encoder { return Create() })
}

// Create a SHTML encoder
func Create() *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.NewTransformer(1, nil),

	}
}

type Encoder struct {
	tx *szenc.Transformer
	th *shtml.Transformer

}

// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {

	metaSHTML, err := enc.th.Transform(enc.tx.GetMeta(zn.InhMeta, evalMeta))
	if err != nil {
		return 0, err
	}
	contentSHTML, err := enc.th.Transform(enc.tx.GetSz(&zn.Ast))
	if err != nil {
		return 0, err
	}
	result := sx.Cons(metaSHTML, contentSHTML)
	return result.Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {

	metaSHTML, err := enc.th.Transform(enc.tx.GetMeta(m, evalMeta))
	if err != nil {
		return 0, err
	}
	return metaSHTML.Print(w)
}

func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {

	hval, err := enc.th.Transform(enc.tx.GetSz(bs))
	if err != nil {
		return 0, err
	}
	return hval.Print(w)
}

// WriteInlines writes an inline slice to the writer
func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {

	hval, err := enc.th.Transform(enc.tx.GetSz(is))
	if err != nil {
		return 0, err
	}
	return hval.Print(w)
}







|



|



|
|
>




|
|
>




>
|



|









>
|



|








>
|



|




>
|



|

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) })
}

// Create a SHTML encoder
func Create(params *encoder.CreateParameter) *Encoder {
	// We need a new transformer every time, because tx.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{
		tx:   szenc.NewTransformer(),
		th:   shtml.NewEvaluator(1, nil),
		lang: params.Lang,
	}
}

type Encoder struct {
	tx   *szenc.Transformer
	th   *shtml.Evaluator
	lang string
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.Ast), &env)
	if err != nil {
		return 0, err
	}
	result := sx.Cons(metaSHTML, contentSHTML)
	return result.Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, metaSHTML)
}

func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, hval)
}

// WriteInlines writes an inline slice to the writer
func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(is), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, hval)
}

Changes to encoder/szenc/szenc.go.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSz, func() encoder.Encoder { return Create() })
}

// Create a S-expr encoder
func Create() *Encoder {
	// We need a new transformer every time, because trans.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{trans: NewTransformer()}







|







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSz, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create a S-expr encoder
func Create() *Encoder {
	// We need a new transformer every time, because trans.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{trans: NewTransformer()}

Changes to encoder/szenc/transform.go.

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	sf := sx.MakeMappedFactory()
	t := Transformer{sf: sf}
	t.zetSyms.InitializeZettelSymbols(sf)

	t.mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
		ast.VerbatimZettel:  t.zetSyms.SymVerbatimZettel,
		ast.VerbatimProg:    t.zetSyms.SymVerbatimProg,
		ast.VerbatimEval:    t.zetSyms.SymVerbatimEval,







|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	sf := sx.MakeMappedFactory(1024)
	t := Transformer{sf: sf}
	t.zetSyms.InitializeZettelSymbols(sf)

	t.mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
		ast.VerbatimZettel:  t.zetSyms.SymVerbatimZettel,
		ast.VerbatimProg:    t.zetSyms.SymVerbatimProg,
		ast.VerbatimEval:    t.zetSyms.SymVerbatimEval,
69
70
71
72
73
74
75

76
77
78
79
80
81
82
		ast.FormatEmph:   t.zetSyms.SymFormatEmph,
		ast.FormatStrong: t.zetSyms.SymFormatStrong,
		ast.FormatDelete: t.zetSyms.SymFormatDelete,
		ast.FormatInsert: t.zetSyms.SymFormatInsert,
		ast.FormatSuper:  t.zetSyms.SymFormatSuper,
		ast.FormatSub:    t.zetSyms.SymFormatSub,
		ast.FormatQuote:  t.zetSyms.SymFormatQuote,

		ast.FormatSpan:   t.zetSyms.SymFormatSpan,
	}
	t.mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{
		ast.LiteralZettel:  t.zetSyms.SymLiteralZettel,
		ast.LiteralProg:    t.zetSyms.SymLiteralProg,
		ast.LiteralInput:   t.zetSyms.SymLiteralInput,
		ast.LiteralOutput:  t.zetSyms.SymLiteralOutput,







>







69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
		ast.FormatEmph:   t.zetSyms.SymFormatEmph,
		ast.FormatStrong: t.zetSyms.SymFormatStrong,
		ast.FormatDelete: t.zetSyms.SymFormatDelete,
		ast.FormatInsert: t.zetSyms.SymFormatInsert,
		ast.FormatSuper:  t.zetSyms.SymFormatSuper,
		ast.FormatSub:    t.zetSyms.SymFormatSub,
		ast.FormatQuote:  t.zetSyms.SymFormatQuote,
		ast.FormatMark:   t.zetSyms.SymFormatMark,
		ast.FormatSpan:   t.zetSyms.SymFormatSpan,
	}
	t.mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{
		ast.LiteralZettel:  t.zetSyms.SymLiteralZettel,
		ast.LiteralProg:    t.zetSyms.SymLiteralProg,
		ast.LiteralInput:   t.zetSyms.SymLiteralInput,
		ast.LiteralOutput:  t.zetSyms.SymLiteralOutput,
135
136
137
138
139
140
141
142
143
144
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
		return t.getInlineSlice(*n)
	case *ast.ParaNode:
		return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara)
	case *ast.VerbatimNode:
		return sx.MakeList(
			mapGetS(t, t.mapVerbatimKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sx.MakeString(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return sx.MakeList(
			t.zetSyms.SymHeading,
			sx.Int64(int64(n.Level)),
			t.getAttributes(n.Attrs),
			sx.MakeString(n.Slug),
			sx.MakeString(n.Fragment),
			t.getInlineSlice(n.Inlines),
		)
	case *ast.HRuleNode:
		return sx.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return sx.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sx.MakeList(t.zetSyms.SymText, sx.MakeString(n.Text))
	case *ast.SpaceNode:
		if t.inVerse {
			return sx.MakeList(t.zetSyms.SymSpace, sx.MakeString(n.Lexeme))
		}
		return sx.MakeList(t.zetSyms.SymSpace)
	case *ast.BreakNode:
		if n.Hard {
			return sx.MakeList(t.zetSyms.SymHard)
		}
		return sx.MakeList(t.zetSyms.SymSoft)
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sx.MakeString(n.Syntax)).
			Cons(t.getReference(n.Ref)).
			Cons(t.getAttributes(n.Attrs)).
			Cons(t.zetSyms.SymEmbed)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sx.MakeString(n.Key)).
			Cons(t.getAttributes(n.Attrs)).
			Cons(t.zetSyms.SymCite)
	case *ast.FootnoteNode:
		text := sx.Nil().Cons(sx.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote))
		return text.Cons(t.getAttributes(n.Attrs)).Cons(t.zetSyms.SymEndnote)
	case *ast.MarkNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sx.MakeString(n.Fragment)).
			Cons(sx.MakeString(n.Slug)).
			Cons(sx.MakeString(n.Mark)).
			Cons(t.zetSyms.SymMark)
	case *ast.FormatNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(t.getAttributes(n.Attrs)).
			Cons(mapGetS(t, t.mapFormatKindS, n.Kind))
	case *ast.LiteralNode:
		return sx.MakeList(
			mapGetS(t, t.mapLiteralKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sx.MakeString(string(n.Content)),
		)
	}
	return sx.MakeList(t.zetSyms.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node)))
}

func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair {
	saveInVerse := t.inVerse
	if rn.Kind == ast.RegionVerse {
		t.inVerse = true
	}







|








|
|















|


|











|







|







|
|
|









|


|







136
137
138
139
140
141
142
143
144
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
		return t.getInlineSlice(*n)
	case *ast.ParaNode:
		return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara)
	case *ast.VerbatimNode:
		return sx.MakeList(
			mapGetS(t, t.mapVerbatimKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sx.String(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return sx.MakeList(
			t.zetSyms.SymHeading,
			sx.Int64(int64(n.Level)),
			t.getAttributes(n.Attrs),
			sx.String(n.Slug),
			sx.String(n.Fragment),
			t.getInlineSlice(n.Inlines),
		)
	case *ast.HRuleNode:
		return sx.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return sx.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sx.MakeList(t.zetSyms.SymText, sx.String(n.Text))
	case *ast.SpaceNode:
		if t.inVerse {
			return sx.MakeList(t.zetSyms.SymSpace, sx.String(n.Lexeme))
		}
		return sx.MakeList(t.zetSyms.SymSpace)
	case *ast.BreakNode:
		if n.Hard {
			return sx.MakeList(t.zetSyms.SymHard)
		}
		return sx.MakeList(t.zetSyms.SymSoft)
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sx.String(n.Syntax)).
			Cons(t.getReference(n.Ref)).
			Cons(t.getAttributes(n.Attrs)).
			Cons(t.zetSyms.SymEmbed)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sx.String(n.Key)).
			Cons(t.getAttributes(n.Attrs)).
			Cons(t.zetSyms.SymCite)
	case *ast.FootnoteNode:
		text := sx.Nil().Cons(sx.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote))
		return text.Cons(t.getAttributes(n.Attrs)).Cons(t.zetSyms.SymEndnote)
	case *ast.MarkNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sx.String(n.Fragment)).
			Cons(sx.String(n.Slug)).
			Cons(sx.String(n.Mark)).
			Cons(t.zetSyms.SymMark)
	case *ast.FormatNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(t.getAttributes(n.Attrs)).
			Cons(mapGetS(t, t.mapFormatKindS, n.Kind))
	case *ast.LiteralNode:
		return sx.MakeList(
			mapGetS(t, t.mapLiteralKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sx.String(string(n.Content)),
		)
	}
	return sx.MakeList(t.zetSyms.SymUnknown, sx.String(fmt.Sprintf("%T %v", node, node)))
}

func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair {
	saveInVerse := t.inVerse
	if rn.Kind == ast.RegionVerse {
		t.inVerse = true
	}
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
func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
	return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align))
}

func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
	var lastObj sx.Object
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sx.MakeString(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sx.MakeList(
		t.zetSyms.SymBLOB,
		t.getInlineSlice(bn.Description),
		sx.MakeString(bn.Syntax),
		lastObj,
	)
}

func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair {
	return t.getInlineSlice(ln.Inlines).Tail().
		Cons(sx.MakeString(ln.Ref.Value)).
		Cons(t.getAttributes(ln.Attrs)).
		Cons(mapGetS(t, t.mapRefStateLink, ln.Ref.State))
}

func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
	tail := t.getInlineSlice(en.Inlines).Tail()
	if en.Syntax == meta.SyntaxSVG {
		tail = tail.Cons(sx.MakeString(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return tail.Cons(sx.MakeString(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB)
}

func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sx.Pair {
	objs := make([]sx.Object, len(*bs))
	for i, n := range *bs {
		objs[i] = t.GetSz(n)
	}







|






|






|







|



|







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
func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
	return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align))
}

func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
	var lastObj sx.Object
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sx.String(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sx.MakeList(
		t.zetSyms.SymBLOB,
		t.getInlineSlice(bn.Description),
		sx.String(bn.Syntax),
		lastObj,
	)
}

func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair {
	return t.getInlineSlice(ln.Inlines).Tail().
		Cons(sx.String(ln.Ref.Value)).
		Cons(t.getAttributes(ln.Attrs)).
		Cons(mapGetS(t, t.mapRefStateLink, ln.Ref.State))
}

func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
	tail := t.getInlineSlice(en.Inlines).Tail()
	if en.Syntax == meta.SyntaxSVG {
		tail = tail.Cons(sx.String(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return tail.Cons(sx.String(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB)
}

func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sx.Pair {
	objs := make([]sx.Object, len(*bs))
	for i, n := range *bs {
		objs[i] = t.GetSz(n)
	}
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
func (t *Transformer) getAttributes(a attrs.Attributes) sx.Object {
	if a.IsEmpty() {
		return sx.Nil()
	}
	keys := a.Keys()
	objs := make([]sx.Object, 0, len(keys))
	for _, k := range keys {
		objs = append(objs, sx.Cons(sx.MakeString(k), sx.MakeString(a[k])))
	}
	return sx.Nil().Cons(sx.MakeList(objs...)).Cons(t.zetSyms.SymQuote)
}

func (t *Transformer) getReference(ref *ast.Reference) *sx.Pair {
	return sx.MakeList(
		t.zetSyms.SymQuote,
		sx.MakeList(
			mapGetS(t, t.mapRefStateS, ref.State),
			sx.MakeString(ref.Value),
		),
	)
}

func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	pairs := m.ComputedPairs()
	objs := make([]sx.Object, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(t, t.mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setObjs := make([]sx.Object, len(setList))
			for i, val := range setList {
				setObjs[i] = sx.MakeString(val)
			}
			obj = sx.MakeList(setObjs...).Cons(t.zetSyms.SymList)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			obj = t.GetSz(&is)
		} else {
			obj = sx.MakeString(p.Value)
		}
		symKey := sx.MakeList(t.zetSyms.SymQuote, t.sf.MustMake(key))
		objs = append(objs, sx.Nil().Cons(obj).Cons(symKey).Cons(symType))
	}
	return sx.MakeList(objs...).Cons(t.zetSyms.SymMeta)
}








|









|
















|






|







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
func (t *Transformer) getAttributes(a attrs.Attributes) sx.Object {
	if a.IsEmpty() {
		return sx.Nil()
	}
	keys := a.Keys()
	objs := make([]sx.Object, 0, len(keys))
	for _, k := range keys {
		objs = append(objs, sx.Cons(sx.String(k), sx.String(a[k])))
	}
	return sx.Nil().Cons(sx.MakeList(objs...)).Cons(t.zetSyms.SymQuote)
}

func (t *Transformer) getReference(ref *ast.Reference) *sx.Pair {
	return sx.MakeList(
		t.zetSyms.SymQuote,
		sx.MakeList(
			mapGetS(t, t.mapRefStateS, ref.State),
			sx.String(ref.Value),
		),
	)
}

func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	pairs := m.ComputedPairs()
	objs := make([]sx.Object, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(t, t.mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setObjs := make([]sx.Object, len(setList))
			for i, val := range setList {
				setObjs[i] = sx.String(val)
			}
			obj = sx.MakeList(setObjs...).Cons(t.zetSyms.SymList)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			obj = t.GetSz(&is)
		} else {
			obj = sx.String(p.Value)
		}
		symKey := sx.MakeList(t.zetSyms.SymQuote, t.sf.MustMake(key))
		objs = append(objs, sx.Nil().Cons(obj).Cons(symKey).Cons(symType))
	}
	return sx.MakeList(objs...).Cons(t.zetSyms.SymMeta)
}

417
418
419
420
421
422
423
424
425
426
427
	var sb strings.Builder
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sx.MakeString(sb.String())
	}
	return sx.MakeString("")
}







|

|

418
419
420
421
422
423
424
425
426
427
428
	var sb strings.Builder
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sx.String(sb.String())
	}
	return sx.String("")
}

Changes to encoder/textenc/textenc.go.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderText, func() encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myTE }

type Encoder struct{}








|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myTE }

type Encoder struct{}

Changes to encoder/zmkenc/zmkenc.go.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderZmk, func() encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myZE }

type Encoder struct{}








|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myZE }

type Encoder struct{}

352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
	}
	var sb strings.Builder
	v.textEnc.WriteInlines(&sb, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
}

var escapeSeqs = strfun.NewSet(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==",
)

func (v *visitor) visitText(tn *ast.TextNode) {
	last := 0
	for i := 0; i < len(tn.Text); i++ {
		if b := tn.Text[i]; b == '\\' {
			v.b.WriteString(tn.Text[last:i])







|







352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
	}
	var sb strings.Builder
	v.textEnc.WriteInlines(&sb, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
}

var escapeSeqs = strfun.NewSet(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##",
)

func (v *visitor) visitText(tn *ast.TextNode) {
	last := 0
	for i := 0; i < len(tn.Text); i++ {
		if b := tn.Text[i]; b == '\\' {
			v.b.WriteString(tn.Text[last:i])
448
449
450
451
452
453
454

455
456
457
458
459
460
461
	ast.FormatEmph:   []byte("__"),
	ast.FormatStrong: []byte("**"),
	ast.FormatInsert: []byte(">>"),
	ast.FormatDelete: []byte("~~"),
	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuote:  []byte(`""`),

	ast.FormatSpan:   []byte("::"),
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))







>







448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
	ast.FormatEmph:   []byte("__"),
	ast.FormatStrong: []byte("**"),
	ast.FormatInsert: []byte(">>"),
	ast.FormatDelete: []byte("~~"),
	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuote:  []byte(`""`),
	ast.FormatMark:   []byte("##"),
	ast.FormatSpan:   []byte("::"),
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))

Changes to encoding/atom/atom.go.

72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
	buf.WriteString("</feed>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	entryUpdated := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil {
			entryUpdated = published.UTC().Format(time.RFC3339)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String()

	buf.WriteString("  <entry>\n")







|







72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
	buf.WriteString("</feed>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	entryUpdated := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
			entryUpdated = published.UTC().Format(time.RFC3339)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String()

	buf.WriteString("  <entry>\n")

Changes to encoding/encoding.go.

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

// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
	maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local)
	for _, m := range ml {
		if val, found := m.Get(api.KeyPublished); found {
			if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil {
				if maxPublished.Before(published) {
					maxPublished = published
				}
			}
		}
	}
	if maxPublished.Year() > 1 {







|







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

// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
	maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local)
	for _, m := range ml {
		if val, found := m.Get(api.KeyPublished); found {
			if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
				if maxPublished.Before(published) {
					maxPublished = published
				}
			}
		}
	}
	if maxPublished.Year() > 1 {

Changes to encoding/rss/rss.go.

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
	buf.WriteString("</channel>\n</rss>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	itemPublished := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil {
			itemPublished = published.UTC().Format(time.RFC1123Z)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String()

	buf.WriteString("  <item>\n")







|







82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
	buf.WriteString("</channel>\n</rss>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	itemPublished := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
			itemPublished = published.UTC().Format(time.RFC1123Z)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String()

	buf.WriteString("  <item>\n")

Changes to evaluator/list.go.

62
63
64
65
66
67
68



69
70
71
72
73
74
75
				ap.max = num
				continue
			}
		}
		if act == "TITLE" && i+1 < len(actions) {
			ap.title = strings.Join(actions[i+1:], " ")
			break



		}
		acts = append(acts, act)
	}
	var firstUnknownKey string
	for _, act := range acts {
		switch act {
		case "ATOM":







>
>
>







62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
				ap.max = num
				continue
			}
		}
		if act == "TITLE" && i+1 < len(actions) {
			ap.title = strings.Join(actions[i+1:], " ")
			break
		}
		if act == "REINDEX" {
			continue
		}
		acts = append(acts, act)
	}
	var firstUnknownKey string
	for _, act := range acts {
		switch act {
		case "ATOM":

Changes to evaluator/metadata.go.

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
		sliceData = meta.ListFromValue(value)
		if len(sliceData) == 0 {
			return nil
		}
	} else {
		sliceData = []string{value}
	}
	var makeLink bool
	switch dt {
	case meta.TypeID, meta.TypeIDSet:
		makeLink = true
	}

	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.SpaceNode{Lexeme: " "})
		}
		tn := &ast.TextNode{Text: val}







<
<
<
|
<







38
39
40
41
42
43
44



45

46
47
48
49
50
51
52
		sliceData = meta.ListFromValue(value)
		if len(sliceData) == 0 {
			return nil
		}
	} else {
		sliceData = []string{value}
	}



	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet


	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.SpaceNode{Lexeme: " "})
		}
		tn := &ast.TextNode{Text: val}

Changes to go.mod.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module zettelstore.de/z

go 1.20

require (
	github.com/fsnotify/fsnotify v1.6.0
	github.com/yuin/goldmark v1.5.5
	golang.org/x/crypto v0.12.0
	golang.org/x/term v0.11.0
	golang.org/x/text v0.12.0
	zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841
	zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284
)

require golang.org/x/sys v0.11.0 // indirect


|


|
|
|
|
|
|
|


|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module zettelstore.de/z

go 1.21

require (
	github.com/fsnotify/fsnotify v1.7.0
	github.com/yuin/goldmark v1.6.0
	golang.org/x/crypto v0.16.0
	golang.org/x/term v0.15.0
	golang.org/x/text v0.14.0
	zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73
	zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207
)

require golang.org/x/sys v0.15.0 // indirect

Changes to go.sum.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841 h1:QlOIlT2cURqM2nYEvL7zOrjiE5/ZA3iAAfslcd6u2PY=
zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841/go.mod h1:MaVH7f0eHaWB5bK0GHNUiPJxKIYGyBk9amBUSbDZM0g=
zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284 h1:26xwZWEjdyL3wObczc/PKugv0EY6mgSH5NUe5kYFCt4=
zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284/go.mod h1:nsWXVrQG8RNKtoXzEMrWXNMdnpfIDU6Hb0pk56KpVKE=
|
|
|
|
|
|
<
|
|
|
|
|
|
|
|
|
|
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=

golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73 h1:LPF8QWip3GRLBzgPdnsJnrvk1sGiMOSpqOwURiax9gg=
zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73/go.mod h1:fN+1WxRorSbHduS0T0B4GI8o82EgFuUWBv6fwsAZF6o=
zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 h1:8ch54z0w53bps6a00NDofEqo3AJ1l7ITXyC3XyLmlY4=
zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207/go.mod h1:Uw3OLM1ufOM4Xe0G51mvkTDUv2okd+HyDBMx+0ZG7ME=

Changes to kernel/impl/cfg.go.

28
29
30
31
32
33
34

35
36
37
38
39
40
41
	"zettelstore.de/z/zettel/meta"
)

type configService struct {
	srvConfig
	mxService sync.RWMutex
	orig      *meta.Meta

}

// Predefined Metadata keys for runtime configuration
// See: https://zettelstore.de/manual/h/00001004020000
const (
	keyDefaultCopyright  = "default-copyright"
	keyDefaultLicense    = "default-license"







>







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
	"zettelstore.de/z/zettel/meta"
)

type configService struct {
	srvConfig
	mxService sync.RWMutex
	orig      *meta.Meta
	manager   box.Manager
}

// Predefined Metadata keys for runtime configuration
// See: https://zettelstore.de/manual/h/00001004020000
const (
	keyDefaultCopyright  = "default-copyright"
	keyDefaultLicense    = "default-license"
88
89
90
91
92
93
94
95




96
97
98
99
100
101
102
103
104
105
106
107
108
109
110




111
112
113
114
115
116
117
		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},




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




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







|
>
>
>
>


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







89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},
		kernel.ConfigSimpleMode:        {"Simple mode", cs.noFrozen(parseBool), true},
		config.KeyShowBackLinks:        {"Show back links", parseString, true},
		config.KeyShowFolgeLinks:       {"Show folge links", parseString, true},
		config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true},
		config.KeyShowSuccessorLinks:   {"Show successor links", parseString, true},
	}
	cs.next = interfaceMap{
		keyDefaultCopyright:            "",
		keyDefaultLicense:              "",
		keyDefaultVisibility:           meta.VisibilityLogin,
		keyExpertMode:                  false,
		config.KeyFooterZettel:         id.Invalid,
		config.KeyHomeZettel:           id.DefaultHomeZid,
		kernel.ConfigInsecureHTML:      config.NoHTML,
		api.KeyLang:                    api.ValueLangEN,
		keyMaxTransclusions:            int64(1024),
		keySiteName:                    "Zettelstore",
		keyYAMLHeader:                  false,
		keyZettelFileSyntax:            nil,
		kernel.ConfigSimpleMode:        false,
		config.KeyShowBackLinks:        "",
		config.KeyShowFolgeLinks:       "",
		config.KeyShowSubordinateLinks: "",
		config.KeyShowSuccessorLinks:   "",
	}
}
func (cs *configService) GetLogger() *logger.Logger { return cs.logger }

func (cs *configService) Start(*myKernel) error {
	cs.logger.Info().Msg("Start Service")
	data := meta.New(id.ConfigurationZid)
130
131
132
133
134
135
136

137
138
139
140
141
142
143
144



145
146
147
148
149
150
151
152
153
154
155
156
157
	return cs.orig != nil
}

func (cs *configService) Stop(*myKernel) {
	cs.logger.Info().Msg("Stop Service")
	cs.mxService.Lock()
	cs.orig = nil

	cs.mxService.Unlock()
}

func (*configService) GetStatistics() []kernel.KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {



	mgr.RegisterObserver(cs.observe)
	cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
}

func (cs *configService) doUpdate(p box.BaseBox) error {
	z, err := p.GetZettel(context.Background(), cs.orig.Zid)
	cs.logger.Trace().Err(err).Msg("got config meta")
	if err != nil {
		return err
	}
	m := z.Meta
	cs.mxService.Lock()
	for _, pair := range cs.orig.Pairs() {







>








>
>
>





|







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
	return cs.orig != nil
}

func (cs *configService) Stop(*myKernel) {
	cs.logger.Info().Msg("Stop Service")
	cs.mxService.Lock()
	cs.orig = nil
	cs.manager = nil
	cs.mxService.Unlock()
}

func (*configService) GetStatistics() []kernel.KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {
	cs.mxService.Lock()
	cs.manager = mgr
	cs.mxService.Unlock()
	mgr.RegisterObserver(cs.observe)
	cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
}

func (cs *configService) doUpdate(p box.BaseBox) error {
	z, err := p.GetZettel(context.Background(), id.ConfigurationZid)
	cs.logger.Trace().Err(err).Msg("got config meta")
	if err != nil {
		return err
	}
	m := z.Meta
	cs.mxService.Lock()
	for _, pair := range cs.orig.Pairs() {
166
167
168
169
170
171
172
173









174
175
176
177
178
179
180
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() { cs.doUpdate(ci.Box) }()









	}
}

// --- config.Config

func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
	if m != nil {







|
>
>
>
>
>
>
>
>
>







179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() {
			cs.mxService.RLock()
			mgr := cs.manager
			cs.mxService.RUnlock()
			if mgr != nil {
				cs.doUpdate(mgr)
			} else {
				cs.doUpdate(ci.Box)
			}
		}()
	}
}

// --- config.Config

func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
	if m != nil {

Changes to kernel/impl/core.go.

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
	cs.next = interfaceMap{
		kernel.CoreDebug:     false,
		kernel.CoreGoArch:    runtime.GOARCH,
		kernel.CoreGoOS:      runtime.GOOS,
		kernel.CoreGoVersion: runtime.Version(),
		kernel.CoreHostname:  "*unknown host*",
		kernel.CorePort:      0,
		kernel.CoreStarted:   time.Now().Local().Format(id.ZidLayout),
		kernel.CoreVerbose:   false,
	}
	if hn, err := os.Hostname(); err == nil {
		cs.next[kernel.CoreHostname] = hn
	}
}








|







77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
	cs.next = interfaceMap{
		kernel.CoreDebug:     false,
		kernel.CoreGoArch:    runtime.GOARCH,
		kernel.CoreGoOS:      runtime.GOOS,
		kernel.CoreGoVersion: runtime.Version(),
		kernel.CoreHostname:  "*unknown host*",
		kernel.CorePort:      0,
		kernel.CoreStarted:   time.Now().Local().Format(id.TimestampLayout),
		kernel.CoreVerbose:   false,
	}
	if hn, err := os.Hostname(); err == nil {
		cs.next[kernel.CoreHostname] = hn
	}
}

Changes to kernel/impl/impl.go.

122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
	}
	return kern
}

func (kern *myKernel) Setup(progname, version string, versionTime time.Time) {
	kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname)
	kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version)
	kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout))
}

func (kern *myKernel) Start(headline, lineServer bool, configFilename string) {
	for _, srvD := range kern.srvs {
		srvD.srv.Freeze()
	}
	if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) {







|







122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
	}
	return kern
}

func (kern *myKernel) Setup(progname, version string, versionTime time.Time) {
	kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname)
	kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version)
	kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.TimestampLayout))
}

func (kern *myKernel) Start(headline, lineServer bool, configFilename string) {
	for _, srvD := range kern.srvs {
		srvD.srv.Freeze()
	}
	if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) {

Changes to kernel/impl/server.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
61
	go func() { lineServer(ln, kern) }()
	return nil
}

func lineServer(ln net.Listener, kern *myKernel) {
	// Something may panic. Ensure a running line service.
	defer func() {
		if r := recover(); r != nil {
			kern.doLogRecover("Line", r)
			go lineServer(ln, kern)
		}
	}()

	for {
		conn, err := ln.Accept()
		if err != nil {
			// handle error
			kern.logger.IfErr(err).Msg("Unable to accept connection")
			break
		}
		go handleLineConnection(conn, kern)
	}
	ln.Close()
}

func handleLineConnection(conn net.Conn, kern *myKernel) {
	// Something may panic. Ensure a running connection.
	defer func() {
		if r := recover(); r != nil {
			kern.doLogRecover("LineConn", r)
			go handleLineConnection(conn, kern)
		}
	}()

	kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console")
	cmds := cmdSession{}
	cmds.initialize(conn, kern)







|
|








|










|
|







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
	go func() { lineServer(ln, kern) }()
	return nil
}

func lineServer(ln net.Listener, kern *myKernel) {
	// Something may panic. Ensure a running line service.
	defer func() {
		if ri := recover(); ri != nil {
			kern.doLogRecover("Line", ri)
			go lineServer(ln, kern)
		}
	}()

	for {
		conn, err := ln.Accept()
		if err != nil {
			// handle error
			kern.logger.Error().Err(err).Msg("Unable to accept connection")
			break
		}
		go handleLineConnection(conn, kern)
	}
	ln.Close()
}

func handleLineConnection(conn net.Conn, kern *myKernel) {
	// Something may panic. Ensure a running connection.
	defer func() {
		if ri := recover(); ri != nil {
			kern.doLogRecover("LineConn", ri)
			go handleLineConnection(conn, kern)
		}
	}()

	kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console")
	cmds := cmdSession{}
	cmds.initialize(conn, kern)

Changes to logger/logger.go.

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

// Warn creates a message suitable for warning the user.
func (l *Logger) Warn() *Message { return newMessage(l, WarnLevel) }

// Error creates a message suitable for errors.
func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) }

// IfErr creates an error message and sets the go error, if there is an error.
func (l *Logger) IfErr(err error) *Message {
	if err != nil {
		return newMessage(l, ErrorLevel).Err(err)
	}
	return nil
}

// Fatal creates a message suitable for fatal errors.
func (l *Logger) Fatal() *Message { return newMessage(l, FatalLevel) }

// Panic creates a message suitable for panicing.
func (l *Logger) Panic() *Message { return newMessage(l, PanicLevel) }

// Mandatory creates a message that will always logged, except when logging







<
<
<
<
<
<
<
<







179
180
181
182
183
184
185








186
187
188
189
190
191
192

// Warn creates a message suitable for warning the user.
func (l *Logger) Warn() *Message { return newMessage(l, WarnLevel) }

// Error creates a message suitable for errors.
func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) }









// Fatal creates a message suitable for fatal errors.
func (l *Logger) Fatal() *Message { return newMessage(l, FatalLevel) }

// Panic creates a message suitable for panicing.
func (l *Logger) Panic() *Message { return newMessage(l, PanicLevel) }

// Mandatory creates a message that will always logged, except when logging

Changes to parser/parser.go.

131
132
133
134
135
136
137

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
	return ast.CreateInlineSliceFromWords(meta.ListFromValue(s)...)
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") }

// ParseDescription returns a suitable description stored in the metadata as an inline slice.

func ParseDescription(m *meta.Meta) ast.InlineSlice {
	if m == nil {
		return nil
	}
	if descr, found := m.Get(api.KeySummary); found {
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return nil
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {







>












|







131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
	return ast.CreateInlineSliceFromWords(meta.ListFromValue(s)...)
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") }

// ParseDescription returns a suitable description stored in the metadata as an inline slice.
// This is done for an image in most cases.
func ParseDescription(m *meta.Meta) ast.InlineSlice {
	if m == nil {
		return nil
	}
	if descr, found := m.Get(api.KeySummary); found {
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return ast.CreateInlineSliceFromWords("Zettel", "without", "title:", m.Zid.String())
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {

Changes to parser/plain/plain.go.

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

import (
	"bytes"
	"strings"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/sx.fossil/sxbuiltins/pprint"
	"zettelstore.de/sx.fossil/sxbuiltins/quote"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)








|
<







12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
package plain

import (
	"bytes"
	"strings"

	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/sx.fossil/sxbuiltins"

	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

133
134
135
136
137
138
139
140
141
142
143
144
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
	}
	// TODO: check proper end </svg>
	return svgSrc
}

func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
	sf := rd.SymbolFactory()
	quote.InstallQuoteReader(rd, sf.MustMake("quote"), '\'')
	quote.InstallQuasiQuoteReader(rd,
		sf.MustMake("quasiquote"), '`',
		sf.MustMake("unquote"), ',',
		sf.MustMake("unquote-splicing"), '@')
	objs, err := rd.ReadAll()
	if err != nil {
		return ast.BlockSlice{
			&ast.VerbatimNode{
				Kind:    ast.VerbatimProg,
				Attrs:   attrs.Attributes{"": syntax},
				Content: inp.ScanLineContent(),
			},
			ast.CreateParaNode(&ast.TextNode{
				Text: err.Error(),
			}),
		}
	}
	result := make(ast.BlockSlice, len(objs))
	for i, obj := range objs {
		var buf bytes.Buffer
		pprint.Print(&buf, obj)
		result[i] = &ast.VerbatimNode{
			Kind:    ast.VerbatimProg,
			Attrs:   attrs.Attributes{"": syntax},
			Content: buf.Bytes(),
		}
	}
	return result







<
<
<
<
<
<
















|







132
133
134
135
136
137
138






139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
	}
	// TODO: check proper end </svg>
	return svgSrc
}

func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))






	objs, err := rd.ReadAll()
	if err != nil {
		return ast.BlockSlice{
			&ast.VerbatimNode{
				Kind:    ast.VerbatimProg,
				Attrs:   attrs.Attributes{"": syntax},
				Content: inp.ScanLineContent(),
			},
			ast.CreateParaNode(&ast.TextNode{
				Text: err.Error(),
			}),
		}
	}
	result := make(ast.BlockSlice, len(objs))
	for i, obj := range objs {
		var buf bytes.Buffer
		sxbuiltins.Print(&buf, obj)
		result[i] = &ast.VerbatimNode{
			Kind:    ast.VerbatimProg,
			Attrs:   attrs.Attributes{"": syntax},
			Content: buf.Bytes(),
		}
	}
	return result

Changes to parser/zettelmark/inline.go.

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
		case '{':
			inp.Next()
			if inp.Ch == '{' {
				in, success = cp.parseEmbed()
			}
		case '%':
			in, success = cp.parseComment()
		case '_', '*', '>', '~', '^', ',', '"', ':':
			in, success = cp.parseFormat()
		case '@', '\'', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '$':
			in, success = cp.parseLiteralMath()
		case '\\':
			return cp.parseBackslash()







|







65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
		case '{':
			inp.Next()
			if inp.Ch == '{' {
				in, success = cp.parseEmbed()
			}
		case '%':
			in, success = cp.parseComment()
		case '_', '*', '>', '~', '^', ',', '"', '#', ':':
			in, success = cp.parseFormat()
		case '@', '\'', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '$':
			in, success = cp.parseLiteralMath()
		case '\\':
			return cp.parseBackslash()
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
		return cp.parseBackslashRest()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp







|







98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
		return cp.parseBackslashRest()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp
414
415
416
417
418
419
420

421
422
423
424
425
426
427
	'_': ast.FormatEmph,
	'*': ast.FormatStrong,
	'>': ast.FormatInsert,
	'~': ast.FormatDelete,
	'^': ast.FormatSuper,
	',': ast.FormatSub,
	'"': ast.FormatQuote,

	':': ast.FormatSpan,
}

func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	kind, ok := mapRuneFormat[fch]







>







414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
	'_': ast.FormatEmph,
	'*': ast.FormatStrong,
	'>': ast.FormatInsert,
	'~': ast.FormatDelete,
	'^': ast.FormatSuper,
	',': ast.FormatSub,
	'"': ast.FormatQuote,
	'#': ast.FormatMark,
	':': ast.FormatSpan,
}

func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	kind, ok := mapRuneFormat[fch]

Changes to parser/zettelmark/zettelmark_test.go.

289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
		{"100%", "(PARA 100%)"},
	})
}

func TestFormat(t *testing.T) {
	t.Parallel()
	// Not for Insert / '>', because collision with quoted list
	for _, ch := range []string{"_", "*", "~", "^", ",", "\"", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
		}))
	}
	for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},







|







|







289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
		{"100%", "(PARA 100%)"},
	})
}

func TestFormat(t *testing.T) {
	t.Parallel()
	// Not for Insert / '>', because collision with quoted list
	for _, ch := range []string{"_", "*", "~", "^", ",", "\"", "#", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
		}))
	}
	for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", "#", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},
986
987
988
989
990
991
992

993
994
995
996
997
998
999
	ast.FormatEmph:   '_',
	ast.FormatStrong: '*',
	ast.FormatInsert: '>',
	ast.FormatDelete: '~',
	ast.FormatSuper:  '^',
	ast.FormatSub:    ',',
	ast.FormatQuote:  '"',

	ast.FormatSpan:   ':',
}

var mapLiteralKind = map[ast.LiteralKind]rune{
	ast.LiteralZettel:  '@',
	ast.LiteralProg:    '`',
	ast.LiteralInput:   '\'',







>







986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
	ast.FormatEmph:   '_',
	ast.FormatStrong: '*',
	ast.FormatInsert: '>',
	ast.FormatDelete: '~',
	ast.FormatSuper:  '^',
	ast.FormatSub:    ',',
	ast.FormatQuote:  '"',
	ast.FormatMark:   '#',
	ast.FormatSpan:   ':',
}

var mapLiteralKind = map[ast.LiteralKind]rune{
	ast.LiteralZettel:  '@',
	ast.LiteralProg:    '`',
	ast.LiteralInput:   '\'',

Changes to query/compiled.go.

42
43
44
45
46
47
48




49
50
51
52
53
54
55
type CompiledTerm struct {
	Match    MetaMatchFunc     // Match on metadata
	Retrieve RetrievePredicate // Retrieve from full-text search
}

// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search.
type RetrievePredicate func(id.Zid) bool





func (c *Compiled) isDeterministic() bool { return c.seed > 0 }

// Result returns a result of the compiled search, that is achievable without iterating through a box.
func (c *Compiled) Result() []*meta.Meta {
	if len(c.startMeta) == 0 {
		// nil -> no directive







>
>
>
>







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
type CompiledTerm struct {
	Match    MetaMatchFunc     // Match on metadata
	Retrieve RetrievePredicate // Retrieve from full-text search
}

// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search.
type RetrievePredicate func(id.Zid) bool

// AlwaysIncluded is a RetrievePredicate that always returns true.
func AlwaysIncluded(id.Zid) bool { return true }
func neverIncluded(id.Zid) bool  { return false }

func (c *Compiled) isDeterministic() bool { return c.seed > 0 }

// Result returns a result of the compiled search, that is achievable without iterating through a box.
func (c *Compiled) Result() []*meta.Meta {
	if len(c.startMeta) == 0 {
		// nil -> no directive

Changes to query/context.go.

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

package query

import (

	"context"

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














>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

package query

import (
	"container/heap"
	"context"

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

83
84
85
86
87
88
89
90
91
92
93


94











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

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
				tasks.addSameTag(ctx, tag, cost)
			}
		}
	}
	return result
}

type ztlCtxTask struct {
	next *ztlCtxTask
	prev *ztlCtxTask
	meta *meta.Meta


	cost int











}

type contextQueue struct {
	port    ContextPort
	seen    id.Set
	first   *ztlCtxTask
	last    *ztlCtxTask
	maxCost int
	limit   int
	tagCost map[string][]*meta.Meta
}

func newQueue(startSeq []*meta.Meta, maxCost, limit int, port ContextPort) *contextQueue {
	result := &contextQueue{
		port:    port,
		seen:    id.NewSet(),
		first:   nil,
		last:    nil,
		maxCost: maxCost,
		limit:   limit,
		tagCost: make(map[string][]*meta.Meta, 1024),
	}

	var prev *ztlCtxTask
	for _, m := range startSeq {
		task := &ztlCtxTask{next: nil, prev: prev, meta: m, cost: 1}
		if prev == nil {
			result.first = task
		} else {
			prev.next = task
		}

		result.last = task
		prev = task
	}
	return result
}

func (zc *contextQueue) addPair(ctx context.Context, key, value string, curCost int, isBackward, isForward bool) {
	if key == api.KeyBack {
		return
	}
	newCost := curCost + contextCost(key)
	if key == api.KeyBackward {
		if isBackward {
			zc.addIDSet(ctx, newCost, value)
		}
		return
	}
	if key == api.KeyForward {
		if isForward {
			zc.addIDSet(ctx, newCost, value)
		}
		return
	}
	hasInverse := meta.Inverse(key) != ""
	if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
		return
	}
	if t := meta.Type(key); t == meta.TypeID {
		zc.addID(ctx, newCost, value)
	} else if t == meta.TypeIDSet {
		zc.addIDSet(ctx, newCost, value)
	}
}

func contextCost(key string) int {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 1
	case api.KeySuccessors, api.KeyPredecessor,
		api.KeySubordinates, api.KeySuperior:
		return 2
	}
	return 3
}

func (zc *contextQueue) addID(ctx context.Context, newCost int, value string) {
	if zc.costMaxed(newCost) {
		return
	}
	if zid, errParse := id.Parse(value); errParse == nil {
		if m, errGetMeta := zc.port.GetMeta(ctx, zid); errGetMeta == nil {
			zc.addMeta(m, newCost)
		}
	}
}
func (zc *contextQueue) addMeta(m *meta.Meta, newCost int) {

	task := &ztlCtxTask{next: nil, prev: nil, meta: m, cost: newCost}
	if zc.first == nil {
		zc.first = task
		zc.last = task
		return
	}

	// Search backward for a task t with at most the same cost
	for t := zc.last; t != nil; t = t.prev {
		if t.cost <= task.cost {
			// Found!
			if t.next != nil {
				t.next.prev = task
			}
			task.next = t.next
			t.next = task
			task.prev = t
			if task.next == nil {
				zc.last = task
			}
			return
		}
	}

	// We have not found a task, therefore the new task is the first one
	task.next = zc.first
	zc.first.prev = task
	zc.first = task
}

func (zc *contextQueue) costMaxed(newCost int) bool {
	// If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all
	// other zettel that are directly reachable, without taking the cost into account.
	// Of course, the limit ist still relevant.
	return (len(zc.seen) > 1 && zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit()
}

func (zc *contextQueue) addIDSet(ctx context.Context, newCost int, value string) {
	elems := meta.ListFromValue(value)
	refCost := referenceCost(newCost, len(elems))
	for _, val := range elems {
		zc.addID(ctx, refCost, val)
	}
}

func referenceCost(baseCost int, numReferences int) int {

	if numReferences < 5 {
		return baseCost + 1
	}
	if numReferences < 9 {
		return baseCost * 2
	}
	if numReferences < 17 {
		return baseCost * 3
	}
	if numReferences < 33 {
		return baseCost * 4
	}
	if numReferences < 65 {
		return baseCost * 5
	}
	return baseCost * numReferences / 8
}

func (zc *contextQueue) addSameTag(ctx context.Context, tag string, baseCost int) {
	tagMetas, found := zc.tagCost[tag]
	if !found {
		q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
		ml, err := zc.port.SelectMeta(ctx, nil, q)
		if err != nil {
			return
		}
		tagMetas = ml
		zc.tagCost[tag] = ml
	}
	cost := tagCost(baseCost, len(tagMetas))
	if zc.costMaxed(cost) {
		return
	}
	for _, m := range tagMetas {
		zc.addMeta(m, cost)
	}
}

func tagCost(baseCost, numTags int) int {
	if numTags < 8 {
		return baseCost + numTags/2
	}
	return (baseCost + 2) * (numTags / 4)
}

func (zc *contextQueue) next() (*meta.Meta, int) {
	if zc.hasLimit() {
		return nil, -1
	}
	for zc.first != nil {
		task := zc.first
		zc.first = task.next
		if zc.first == nil {
			zc.last = nil
		} else {
			zc.first.prev = nil
		}
		m := task.meta
		zid := m.Zid
		_, found := zc.seen[zid]
		if found {
			continue
		}
		zc.seen.Zid(zid)
		return m, task.cost
	}
	return nil, -1
}

func (zc *contextQueue) hasLimit() bool {
	limit := zc.limit
	return limit > 0 && len(zc.seen) >= limit
}







|
|
<

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


|


|
<





|
|


<
<





|

|
<
<
<
<
|
>
|
<
<



|






|





|








|

|














|
|



|
|



|
>
|
<
<
<
<

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



|


|



|




>
|

<
|

<
|

<
|

<
|





|
|


|




|


|



|










|
|


|
|
<
<
<
<
<
<
|

|
<


|
|




|
|
|

84
85
86
87
88
89
90
91
92

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

114
115
116
117
118
119
120
121
122


123
124
125
126
127
128
129
130




131
132
133


134
135
136
137
138
139
140
141
142
143
144
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
				tasks.addSameTag(ctx, tag, cost)
			}
		}
	}
	return result
}

type ztlCtxItem struct {
	cost int

	meta *meta.Meta
}
type ztlCtxQueue []ztlCtxItem

func (q ztlCtxQueue) Len() int           { return len(q) }
func (q ztlCtxQueue) Less(i, j int) bool { return q[i].cost < q[j].cost }
func (q ztlCtxQueue) Swap(i, j int)      { q[i], q[j] = q[j], q[i] }
func (q *ztlCtxQueue) Push(x any)        { *q = append(*q, x.(ztlCtxItem)) }
func (q *ztlCtxQueue) Pop() any {
	old := *q
	n := len(old)
	item := old[n-1]
	old[n-1].meta = nil // avoid memory leak
	*q = old[0 : n-1]
	return item
}

type contextTask struct {
	port    ContextPort
	seen    id.Set
	queue   ztlCtxQueue

	maxCost int
	limit   int
	tagCost map[string][]*meta.Meta
}

func newQueue(startSeq []*meta.Meta, maxCost, limit int, port ContextPort) *contextTask {
	result := &contextTask{
		port:    port,
		seen:    id.NewSet(),


		maxCost: maxCost,
		limit:   limit,
		tagCost: make(map[string][]*meta.Meta, 1024),
	}

	queue := make(ztlCtxQueue, 0, len(startSeq))
	for _, m := range startSeq {
		queue = append(queue, ztlCtxItem{cost: 1, meta: m})




	}
	heap.Init(&queue)
	result.queue = queue


	return result
}

func (ct *contextTask) addPair(ctx context.Context, key, value string, curCost int, isBackward, isForward bool) {
	if key == api.KeyBack {
		return
	}
	newCost := curCost + contextCost(key)
	if key == api.KeyBackward {
		if isBackward {
			ct.addIDSet(ctx, newCost, value)
		}
		return
	}
	if key == api.KeyForward {
		if isForward {
			ct.addIDSet(ctx, newCost, value)
		}
		return
	}
	hasInverse := meta.Inverse(key) != ""
	if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
		return
	}
	if t := meta.Type(key); t == meta.TypeID {
		ct.addID(ctx, newCost, value)
	} else if t == meta.TypeIDSet {
		ct.addIDSet(ctx, newCost, value)
	}
}

func contextCost(key string) int {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 1
	case api.KeySuccessors, api.KeyPredecessor,
		api.KeySubordinates, api.KeySuperior:
		return 2
	}
	return 3
}

func (ct *contextTask) addID(ctx context.Context, newCost int, value string) {
	if ct.costMaxed(newCost) {
		return
	}
	if zid, errParse := id.Parse(value); errParse == nil {
		if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil {
			ct.addMeta(m, newCost)
		}
	}
}
func (ct *contextTask) addMeta(m *meta.Meta, newCost int) {
	if _, found := ct.seen[m.Zid]; !found {
		heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m})




	}
}























func (ct *contextTask) costMaxed(newCost int) bool {
	// If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all
	// other zettel that are directly reachable, without taking the cost into account.
	// Of course, the limit ist still relevant.
	return (len(ct.seen) > 1 && ct.maxCost > 0 && newCost > ct.maxCost) || ct.hasLimit()
}

func (ct *contextTask) addIDSet(ctx context.Context, newCost int, value string) {
	elems := meta.ListFromValue(value)
	refCost := referenceCost(newCost, len(elems))
	for _, val := range elems {
		ct.addID(ctx, refCost, val)
	}
}

func referenceCost(baseCost int, numReferences int) int {
	switch {
	case numReferences < 5:
		return baseCost + 1

	case numReferences < 9:
		return baseCost * 2

	case numReferences < 17:
		return baseCost * 3

	case numReferences < 33:
		return baseCost * 4

	case numReferences < 65:
		return baseCost * 5
	}
	return baseCost * numReferences / 8
}

func (ct *contextTask) addSameTag(ctx context.Context, tag string, baseCost int) {
	tagMetas, found := ct.tagCost[tag]
	if !found {
		q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
		ml, err := ct.port.SelectMeta(ctx, nil, q)
		if err != nil {
			return
		}
		tagMetas = ml
		ct.tagCost[tag] = ml
	}
	cost := tagCost(baseCost, len(tagMetas))
	if ct.costMaxed(cost) {
		return
	}
	for _, m := range tagMetas {
		ct.addMeta(m, cost)
	}
}

func tagCost(baseCost, numTags int) int {
	if numTags < 8 {
		return baseCost + numTags/2
	}
	return (baseCost + 2) * (numTags / 4)
}

func (ct *contextTask) next() (*meta.Meta, int) {
	if ct.hasLimit() {
		return nil, -1
	}
	for len(ct.queue) > 0 {
		item := heap.Pop(&ct.queue).(ztlCtxItem)






		m := item.meta
		zid := m.Zid
		if _, found := ct.seen[zid]; found {

			continue
		}
		ct.seen.Add(zid)
		return m, item.cost
	}
	return nil, -1
}

func (ct *contextTask) hasLimit() bool {
	limit := ct.limit
	return limit > 0 && len(ct.seen) >= limit
}

Changes to query/parser.go.

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

type parserState struct {
	inp *input.Input
}

func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS }
func (ps *parserState) acceptSingleKw(s string) bool {
	if ps.inp.Accept(s) && (ps.isSpace() || ps.mustStop()) {
		return true
	}
	return false
}
func (ps *parserState) acceptKwArgs(s string) bool {
	if ps.inp.Accept(s) && ps.isSpace() {
		ps.skipSpace()







|







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

type parserState struct {
	inp *input.Input
}

func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS }
func (ps *parserState) acceptSingleKw(s string) bool {
	if ps.inp.Accept(s) && (ps.isSpace() || ps.isActionSep() || ps.mustStop()) {
		return true
	}
	return false
}
func (ps *parserState) acceptKwArgs(s string) bool {
	if ps.inp.Accept(s) && ps.isSpace() {
		ps.skipSpace()
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
	for {
		pos := inp.Pos
		zid, found := ps.scanZid()
		if !found {
			inp.SetPos(pos)
			break
		}
		if !zidSet.Contains(zid) {
			zidSet.Zid(zid)
			q = createIfNeeded(q)
			q.zids = append(q.zids, zid)
		}
		ps.skipSpace()
		if ps.mustStop() {
			q.zids = nil
			break







|
|







79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
	for {
		pos := inp.Pos
		zid, found := ps.scanZid()
		if !found {
			inp.SetPos(pos)
			break
		}
		if !zidSet.ContainsOrNil(zid) {
			zidSet.Add(zid)
			q = createIfNeeded(q)
			q.zids = append(q.zids, zid)
		}
		ps.skipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {
			if hasContext {
				inp.SetPos(pos)
				break
			}
			q = ps.parseContext(q, pos)
			hasContext = true
			continue
		}
		inp.SetPos(pos)
		if q == nil || len(q.zids) == 0 {
			break
		}







|







103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {
			if hasContext {
				inp.SetPos(pos)
				break
			}
			q = ps.parseContext(q)
			hasContext = true
			continue
		}
		inp.SetPos(pos)
		if q == nil || len(q.zids) == 0 {
			break
		}
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
		if ps.acceptKwArgs(api.LimitDirective) {
			if s, ok := ps.parseLimit(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if isActionSep(inp.Ch) {
			q = ps.parseActions(q)
			break
		}
		q = ps.parseText(q)
	}
	return q
}

func (ps *parserState) parseContext(q *Query, pos int) *Query {
	inp := ps.inp

	if q == nil || len(q.zids) == 0 {
		ps.skipSpace()
		if ps.mustStop() {
			inp.SetPos(pos)
			return q
		}
		zid, ok := ps.scanZid()
		if !ok {
			inp.SetPos(pos)
			return q
		}
		q = createIfNeeded(q)
		q.zids = append(q.zids, zid)
	}

	spec := &ContextSpec{}
	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos = inp.Pos
		if ps.acceptSingleKw(api.BackwardDirective) {
			spec.Direction = ContextDirBackward
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.ForwardDirective) {
			spec.Direction = ContextDirForward







|








|

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






|







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
		if ps.acceptKwArgs(api.LimitDirective) {
			if s, ok := ps.parseLimit(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.isActionSep() {
			q = ps.parseActions(q)
			break
		}
		q = ps.parseText(q)
	}
	return q
}

func (ps *parserState) parseContext(q *Query) *Query {
	inp := ps.inp
















	spec := &ContextSpec{}
	for {
		ps.skipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.BackwardDirective) {
			spec.Direction = ContextDirBackward
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.ForwardDirective) {
			spec.Direction = ContextDirForward
242
243
244
245
246
247
248

249
250
251
252
253
254
255
				continue
			}
		}

		inp.SetPos(pos)
		break
	}

	q.directives = append(q.directives, spec)
	return q
}
func (ps *parserState) parseCost(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false







>







226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
				continue
			}
		}

		inp.SetPos(pos)
		break
	}
	q = createIfNeeded(q)
	q.directives = append(q.directives, spec)
	return q
}
func (ps *parserState) parseCost(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
	}
	text, key := ps.scanSearchTextOrKey(hasOp)
	if len(key) > 0 {
		// Assert: hasOp == false
		op, hasOp = ps.scanSearchOp()
		// Assert hasOp == true
		if op == cmpExist || op == cmpNotExist {
			if ps.isSpace() || isActionSep(inp.Ch) || ps.mustStop() {
				return q.addKey(string(key), op)
			}
			ps.inp.SetPos(pos)
			hasOp = false
			text = ps.scanWord()
			key = nil
		} else {







|







363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
	}
	text, key := ps.scanSearchTextOrKey(hasOp)
	if len(key) > 0 {
		// Assert: hasOp == false
		op, hasOp = ps.scanSearchOp()
		// Assert hasOp == true
		if op == cmpExist || op == cmpNotExist {
			if ps.isSpace() || ps.isActionSep() || ps.mustStop() {
				return q.addKey(string(key), op)
			}
			ps.inp.SetPos(pos)
			hasOp = false
			text = ps.scanWord()
			key = nil
		} else {
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
}

func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) {
	inp := ps.inp
	pos := inp.Pos
	allowKey := !hasOp

	for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() {
		if allowKey {
			switch inp.Ch {
			case searchOperatorNotChar, existOperatorChar,
				searchOperatorEqualChar, searchOperatorHasChar,
				searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar,
				searchOperatorLessChar, searchOperatorGreaterChar:
				allowKey = false
				if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) {
					return nil, key
				}
			}
		}
		inp.Next()
	}
	return inp.Src[pos:inp.Pos], nil
}

func (ps *parserState) scanWord() []byte {
	inp := ps.inp
	pos := inp.Pos
	for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() {
		inp.Next()
	}
	return inp.Src[pos:inp.Pos]
}

func (ps *parserState) scanPosInt() (int, bool) {
	word := ps.scanWord()







|




















|







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
}

func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) {
	inp := ps.inp
	pos := inp.Pos
	allowKey := !hasOp

	for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() {
		if allowKey {
			switch inp.Ch {
			case searchOperatorNotChar, existOperatorChar,
				searchOperatorEqualChar, searchOperatorHasChar,
				searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar,
				searchOperatorLessChar, searchOperatorGreaterChar:
				allowKey = false
				if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) {
					return nil, key
				}
			}
		}
		inp.Next()
	}
	return inp.Src[pos:inp.Pos], nil
}

func (ps *parserState) scanWord() []byte {
	inp := ps.inp
	pos := inp.Pos
	for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() {
		inp.Next()
	}
	return inp.Src[pos:inp.Pos]
}

func (ps *parserState) scanPosInt() (int, bool) {
	word := ps.scanWord()
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541












542

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

func (ps *parserState) isSpace() bool {
	return isSpace(ps.inp.Ch)
}

func isSpace(ch rune) bool {
	switch ch {
	case input.EOS:
		return false
	case ' ', '\t', '\n', '\r':
		return true
	}
	return input.IsSpace(ch)
}

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













func isActionSep(ch rune) bool { return ch == actionSeparatorChar }








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






>
>
>
>
>
>
>
>
>
>
>
>
|
>
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
	}
	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/parser_test.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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

func TestParser(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		spec string
		exp  string
	}{
		{"1", "1"}, // Just a number will transform to search for that numer in all zettel

		{"1 IDENT", "00000000000001 IDENT"},
		{"IDENT", "IDENT"},


		{"1 ITEMS", "00000000000001 ITEMS"},
		{"ITEMS", "ITEMS"},

		{"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"},
		{"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"},
		{"00000000000001 CONTEXT", "00000000000001 CONTEXT"},
		{"100000000000001 CONTEXT", "100000000000001 CONTEXT"},
		{"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"},
		{"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"},
		{"1 CONTEXT COST ", "00000000000001 CONTEXT COST"},
		{"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"},
		{"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"},
		{"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"},
		{"1 CONTEXT |  N", "00000000000001 CONTEXT | N"},
		{"1 1 CONTEXT", "00000000000001 CONTEXT"},
		{"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"},
		{"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"},


		{"CONTEXT 0", "CONTEXT 0"}, {"CONTEXT 1", "00000000000001 CONTEXT"},
		{"CONTEXT 00000000000001", "00000000000001 CONTEXT"},
		{"CONTEXT 100000000000001", "CONTEXT 100000000000001"},
		{"CONTEXT 1 BACKWARD", "00000000000001 CONTEXT BACKWARD"},
		{"CONTEXT 1 FORWARD", "00000000000001 CONTEXT FORWARD"},
		{"CONTEXT 1 COST 3", "00000000000001 CONTEXT COST 3"}, {"CONTEXT 1 COST x", "00000000000001 CONTEXT COST x"},
		{"CONTEXT 1 MAX 5", "00000000000001 CONTEXT MAX 5"}, {"CONTEXT 1 MAX y", "00000000000001 CONTEXT MAX y"},
		{"CONTEXT 1 MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"},
		{"CONTEXT 1 |  N", "00000000000001 CONTEXT | N"},

		{"1 UNLINKED", "00000000000001 UNLINKED"},
		{"UNLINKED", "UNLINKED"},
		{"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"},
		{"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"},

		{"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"},







|



>


















>

|
<
<
<
<
<
<
<
<







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








51
52
53
54
55
56
57

func TestParser(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		spec string
		exp  string
	}{
		{"1", "1"}, // Just a number will transform to search for that number in all zettel

		{"1 IDENT", "00000000000001 IDENT"},
		{"IDENT", "IDENT"},
		{"1 IDENT|REINDEX", "00000000000001 IDENT | REINDEX"},

		{"1 ITEMS", "00000000000001 ITEMS"},
		{"ITEMS", "ITEMS"},

		{"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"},
		{"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"},
		{"00000000000001 CONTEXT", "00000000000001 CONTEXT"},
		{"100000000000001 CONTEXT", "100000000000001 CONTEXT"},
		{"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"},
		{"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"},
		{"1 CONTEXT COST ", "00000000000001 CONTEXT COST"},
		{"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"},
		{"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"},
		{"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"},
		{"1 CONTEXT |  N", "00000000000001 CONTEXT | N"},
		{"1 1 CONTEXT", "00000000000001 CONTEXT"},
		{"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"},
		{"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"},
		{"1 CONTEXT|N", "00000000000001 CONTEXT | N"},

		{"CONTEXT 0", "CONTEXT 0"},









		{"1 UNLINKED", "00000000000001 UNLINKED"},
		{"UNLINKED", "UNLINKED"},
		{"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"},
		{"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"},

		{"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"},

Changes to query/query.go.

10
11
12
13
14
15
16

17
18
19
20
21
22
23

// Package query provides a query for zettel.
package query

import (
	"context"
	"math/rand"


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

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {







>







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

// Package query provides a query for zettel.
package query

import (
	"context"
	"math/rand"
	"slices"

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

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {
241
242
243
244
245
246
247















248
249
250
251
252
253
254
func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) }

func (q *Query) addKey(key string, op compareOp) *Query {
	q = createIfNeeded(q)
	q.terms[len(q.terms)-1].addKey(key, op)
	return q
}
















// SetPreMatch sets the pre-selection predicate.
func (q *Query) SetPreMatch(preMatch MetaMatchFunc) *Query {
	q = createIfNeeded(q)
	if q.preMatch != nil {
		panic("search PreMatch already set")
	}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) }

func (q *Query) addKey(key string, op compareOp) *Query {
	q = createIfNeeded(q)
	q.terms[len(q.terms)-1].addKey(key, op)
	return q
}

func (q *Query) GetMetaValues(key string) (vals []string) {
	if q == nil {
		return nil
	}
	for _, term := range q.terms {
		if mvs, hasMv := term.mvals[key]; hasMv {
			for _, ev := range mvs {
				vals = append(vals, ev.value)
			}
		}
	}
	slices.Sort(vals)
	return slices.Compact(vals)
}

// SetPreMatch sets the pre-selection predicate.
func (q *Query) SetPreMatch(preMatch MetaMatchFunc) *Query {
	q = createIfNeeded(q)
	if q.preMatch != nil {
		panic("search PreMatch already set")
	}
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
// for its results and returns a matching predicate.
func (q *Query) RetrieveAndCompile(_ context.Context, searcher Searcher, metaSeq []*meta.Meta) Compiled {
	if q == nil {
		return Compiled{
			PreMatch: matchAlways,
			Terms: []CompiledTerm{{
				Match:    matchAlways,
				Retrieve: alwaysIncluded,
			}}}
	}
	q = q.Clone()

	preMatch := q.preMatch
	if preMatch == nil {
		preMatch = matchAlways







|







348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
// for its results and returns a matching predicate.
func (q *Query) RetrieveAndCompile(_ context.Context, searcher Searcher, metaSeq []*meta.Meta) Compiled {
	if q == nil {
		return Compiled{
			PreMatch: matchAlways,
			Terms: []CompiledTerm{{
				Match:    matchAlways,
				Retrieve: AlwaysIncluded,
			}}}
	}
	q = q.Clone()

	preMatch := q.preMatch
	if preMatch == nil {
		preMatch = matchAlways
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
	for _, term := range q.terms {
		cTerm := term.retrieveAndCompileTerm(searcher, startSet)
		if cTerm.Retrieve == nil {
			if cTerm.Match == nil {
				// no restriction on match/retrieve -> all will match
				result.Terms = []CompiledTerm{{
					Match:    matchAlways,
					Retrieve: alwaysIncluded,
				}}
				break
			}
			cTerm.Retrieve = alwaysIncluded
		}
		if cTerm.Match == nil {
			cTerm.Match = matchAlways
		}
		result.Terms = append(result.Terms, cTerm)
	}
	return result
}

func metaList2idSet(ml []*meta.Meta) id.Set {
	if ml == nil {
		return nil
	}
	result := id.NewSetCap(len(ml))
	for _, m := range ml {
		result = result.Zid(m.Zid)
	}
	return result
}

func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet id.Set) CompiledTerm {
	match := ct.compileMeta() // Match might add some searches
	var pred RetrievePredicate
	if searcher != nil {
		pred = ct.retrieveIndex(searcher)
		if startSet != nil {
			if pred == nil {
				pred = startSet.Contains
			} else {
				predSet := id.NewSetCap(len(startSet))
				for zid := range startSet {
					if pred(zid) {
						predSet = predSet.Zid(zid)
					}
				}
				pred = predSet.Contains
			}
		}
	}
	return CompiledTerm{Match: match, Retrieve: pred}
}

// retrieveIndex and return a predicate to ask for results.







|



|















|











|




|


|







378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
	for _, term := range q.terms {
		cTerm := term.retrieveAndCompileTerm(searcher, startSet)
		if cTerm.Retrieve == nil {
			if cTerm.Match == nil {
				// no restriction on match/retrieve -> all will match
				result.Terms = []CompiledTerm{{
					Match:    matchAlways,
					Retrieve: AlwaysIncluded,
				}}
				break
			}
			cTerm.Retrieve = AlwaysIncluded
		}
		if cTerm.Match == nil {
			cTerm.Match = matchAlways
		}
		result.Terms = append(result.Terms, cTerm)
	}
	return result
}

func metaList2idSet(ml []*meta.Meta) id.Set {
	if ml == nil {
		return nil
	}
	result := id.NewSetCap(len(ml))
	for _, m := range ml {
		result = result.Add(m.Zid)
	}
	return result
}

func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet id.Set) CompiledTerm {
	match := ct.compileMeta() // Match might add some searches
	var pred RetrievePredicate
	if searcher != nil {
		pred = ct.retrieveIndex(searcher)
		if startSet != nil {
			if pred == nil {
				pred = startSet.ContainsOrNil
			} else {
				predSet := id.NewSetCap(len(startSet))
				for zid := range startSet {
					if pred(zid) {
						predSet = predSet.Add(zid)
					}
				}
				pred = predSet.ContainsOrNil
			}
		}
	}
	return CompiledTerm{Match: match, Retrieve: pred}
}

// retrieveIndex and return a predicate to ask for results.
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
	}

	positives := retrievePositives(normCalls, plainCalls)
	if positives == nil {
		// No positive search for words, must contain only words for a negative search.
		// Otherwise len(search) == 0 (see above)
		negatives := retrieveNegatives(negCalls)
		return func(zid id.Zid) bool { return !negatives.Contains(zid) }
	}
	if len(positives) == 0 {
		// Positive search didn't found anything. We can omit the negative search.
		return neverIncluded
	}
	if len(negCalls) == 0 {
		// Positive search found something, but there is no negative search.
		return positives.Contains
	}
	negatives := retrieveNegatives(negCalls)
	if negatives == nil {
		return positives.Contains
	}
	return func(zid id.Zid) bool {
		return positives.Contains(zid) && !negatives.Contains(zid)
	}
}

// Limit returns only s.GetLimit() elements of the given list.
func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta {
	if q == nil {
		return metaList
	}
	return limitElements(metaList, q.limit)
}







|







|



|


|










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
	}

	positives := retrievePositives(normCalls, plainCalls)
	if positives == nil {
		// No positive search for words, must contain only words for a negative search.
		// Otherwise len(search) == 0 (see above)
		negatives := retrieveNegatives(negCalls)
		return func(zid id.Zid) bool { return !negatives.ContainsOrNil(zid) }
	}
	if len(positives) == 0 {
		// Positive search didn't found anything. We can omit the negative search.
		return neverIncluded
	}
	if len(negCalls) == 0 {
		// Positive search found something, but there is no negative search.
		return positives.ContainsOrNil
	}
	negatives := retrieveNegatives(negCalls)
	if negatives == nil {
		return positives.ContainsOrNil
	}
	return func(zid id.Zid) bool {
		return positives.ContainsOrNil(zid) && !negatives.ContainsOrNil(zid)
	}
}

// Limit returns only s.GetLimit() elements of the given list.
func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta {
	if q == nil {
		return metaList
	}
	return limitElements(metaList, q.limit)
}

Changes to query/retrieve.go.

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
		if pred(s, k.s) {
			delete(scm, k)
		}
	}
	scm[searchOp{s: s, op: op}] = sf
}

func alwaysIncluded(id.Zid) bool { return true }
func neverIncluded(id.Zid) bool  { return false }

func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) {
	normCalls = make(searchCallMap, len(search))
	negCalls = make(searchCallMap, len(search))
	for _, val := range search {
		for _, word := range strfun.NormalizeWords(val.value) {
			if cmpOp := val.op; cmpOp.isNegated() {
				cmpOp = cmpOp.negate()







<
<
<







58
59
60
61
62
63
64



65
66
67
68
69
70
71
		if pred(s, k.s) {
			delete(scm, k)
		}
	}
	scm[searchOp{s: s, op: op}] = sf
}




func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) {
	normCalls = make(searchCallMap, len(search))
	negCalls = make(searchCallMap, len(search))
	for _, val := range search {
		for _, word := range strfun.NormalizeWords(val.value) {
			if cmpOp := val.op; cmpOp.isNegated() {
				cmpOp = cmpOp.negate()
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
			if result, found := cache[c]; found {
				normResult = normResult.IntersectOrSet(result)
				continue
			}
		}
		normResult = normResult.IntersectOrSet(sf(c.s))
	}
	return normResult.Add(plainResult)
}

func isSuperset(normCalls, plainCalls searchCallMap) bool {
	for c := range plainCalls {
		if _, found := normCalls[c]; !found {
			return false
		}
	}
	return true
}

func retrieveNegatives(negCalls searchCallMap) id.Set {
	var negatives id.Set
	for val, sf := range negCalls {
		negatives = negatives.Add(sf(val.s))
	}
	return negatives
}

func getSearchFunc(searcher Searcher, op compareOp) searchFunc {
	switch op {
	case cmpEqual:







|














|







129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
			if result, found := cache[c]; found {
				normResult = normResult.IntersectOrSet(result)
				continue
			}
		}
		normResult = normResult.IntersectOrSet(sf(c.s))
	}
	return normResult.Copy(plainResult)
}

func isSuperset(normCalls, plainCalls searchCallMap) bool {
	for c := range plainCalls {
		if _, found := normCalls[c]; !found {
			return false
		}
	}
	return true
}

func retrieveNegatives(negCalls searchCallMap) id.Set {
	var negatives id.Set
	for val, sf := range negCalls {
		negatives = negatives.Copy(sf(val.s))
	}
	return negatives
}

func getSearchFunc(searcher Searcher, op compareOp) searchFunc {
	switch op {
	case cmpEqual:

Changes to query/select.go.

105
106
107
108
109
110
111
112
113
114
115


116
117
118
119
120
121
122
func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc {
	if len(values) == 0 {
		return nil
	}
	switch meta.Type(key) {
	case meta.TypeCredential:
		return matchValueNever
	case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout
		return createMatchIDFunc(values, addSearch)
	case meta.TypeIDSet:
		return createMatchIDSetFunc(values, addSearch)


	case meta.TypeNumber:
		return createMatchNumberFunc(values, addSearch)
	case meta.TypeTagSet:
		return createMatchTagSetFunc(values, addSearch)
	case meta.TypeWord:
		return createMatchWordFunc(values, addSearch)
	case meta.TypeWordSet:







|



>
>







105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc {
	if len(values) == 0 {
		return nil
	}
	switch meta.Type(key) {
	case meta.TypeCredential:
		return matchValueNever
	case meta.TypeID:
		return createMatchIDFunc(values, addSearch)
	case meta.TypeIDSet:
		return createMatchIDSetFunc(values, addSearch)
	case meta.TypeTimestamp:
		return createMatchTimestampFunc(values, addSearch)
	case meta.TypeNumber:
		return createMatchNumberFunc(values, addSearch)
	case meta.TypeTagSet:
		return createMatchTagSetFunc(values, addSearch)
	case meta.TypeWord:
		return createMatchWordFunc(values, addSearch)
	case meta.TypeWordSet:
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
					return false
				}
			}
		}
		return true
	}
}













func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToNumberPredicates(values, addSearch)
	return func(value string) bool {
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := valuesToWordSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch)
	return func(value string) bool {
		tags := meta.ListFromValue(value)
		// Remove leading '#' from each tag
		for i, tag := range tags {
			tags[i] = meta.CleanTag(tag)
		}
		for _, preds := range predList {
			for _, pred := range preds {
				if !pred(tags) {
					return false
				}
			}
		}







>
>
>
>
>
>
>
>
>
>
>
>
















|
<
<
<
<







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
					return false
				}
			}
		}
		return true
	}
}
func createMatchTimestampFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToTimestampPredicates(values, addSearch)
	return func(value string) bool {
		value = meta.ExpandTimestamp(value)
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToNumberPredicates(values, addSearch)
	return func(value string) bool {
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := valuesToWordSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch)
	return func(value string) bool {
		tags := meta.TagsFromValue(value)




		for _, preds := range predList {
			for _, pred := range preds {
				if !pred(tags) {
					return false
				}
			}
		}
385
386
387
388
389
390
391





























392
393
394
395
396
397
398
}

func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createIDCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	return createWordCompareFunc(cmpVal, cmpOp)
}






























func valuesToNumberPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		switch op := disambiguatedNumberOp(v.op); op {
		case cmpEqual, cmpNotEqual, cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
			iValue, err := strconv.ParseInt(v.value, 10, 64)







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
}

func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createIDCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	return createWordCompareFunc(cmpVal, cmpOp)
}

func valuesToTimestampPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		value := meta.ExpandTimestamp(v.value)
		switch op := disambiguatedTimestampOp(v.op); op {
		case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
			if isDigits(value) {
				// Never add the value to search.
				result[i] = createTimestampCompareFunc(value, op)
				continue
			}
			fallthrough
		default:
			// Otherwise compare as a word.
			if !op.isNegated() {
				addSearch(v) // addSearch only for positive selections
			}
			result[i] = createWordCompareFunc(value, op)
		}
	}
	return result
}

func disambiguatedTimestampOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createTimestampCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	return createWordCompareFunc(cmpVal, cmpOp)
}

func valuesToNumberPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		switch op := disambiguatedNumberOp(v.op); op {
		case cmpEqual, cmpNotEqual, cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
			iValue, err := strconv.ParseInt(v.value, 10, 64)

Changes to query/sorter.go.

53
54
55
56
57
58
59



60
61
62
63
64















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









80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
	keyType := meta.Type(key)
	if key == api.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
	}



	if keyType == meta.TypeNumber {
		return createSortNumberFunc(ml, key, descending)
	}
	return createSortStringFunc(ml, key, descending)
}
















func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := getNum(ml[i], key)
			jVal, jOk := getNum(ml[j], key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := getNum(ml[i], key)
		jVal, jOk := getNum(ml[j], key)
		return (iOk && (!jOk || iVal < jVal)) || !jOk
	}
}










func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := ml[i].Get(key)
		jVal, jOk := ml[j].Get(key)
		return (iOk && (!jOk || iVal < jVal)) || !jOk
	}
}

func getNum(m *meta.Meta, key string) (int64, bool) {
	if s, ok := m.Get(key); ok {
		if i, err := strconv.ParseInt(s, 10, 64); err == nil {
			return i, true
		}
	}
	return 0, false
}







>
>
>





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>















>
>
>
>
>
>
>
>
>















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









	keyType := meta.Type(key)
	if key == api.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
	}
	if keyType == meta.TypeTimestamp {
		return createSortTimestampFunc(ml, key, descending)
	}
	if keyType == meta.TypeNumber {
		return createSortNumberFunc(ml, key, descending)
	}
	return createSortStringFunc(ml, key, descending)
}

func createSortTimestampFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)
			return (iOk && (!jOk || meta.ExpandTimestamp(iVal) > meta.ExpandTimestamp(jVal))) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := ml[i].Get(key)
		jVal, jOk := ml[j].Get(key)
		return (iOk && (!jOk || meta.ExpandTimestamp(iVal) < meta.ExpandTimestamp(jVal))) || !jOk
	}
}

func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := getNum(ml[i], key)
			jVal, jOk := getNum(ml[j], key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := getNum(ml[i], key)
		jVal, jOk := getNum(ml[j], key)
		return (iOk && (!jOk || iVal < jVal)) || !jOk
	}
}

func getNum(m *meta.Meta, key string) (int64, bool) {
	if s, ok := m.Get(key); ok {
		if i, err := strconv.ParseInt(s, 10, 64); err == nil {
			return i, true
		}
	}
	return 0, false
}

func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := ml[i].Get(key)
		jVal, jOk := ml[j].Get(key)
		return (iOk && (!jOk || iVal < jVal)) || !jOk
	}
}









Added testdata/testbox/20230929102100.zettel.















>
>
>
>
>
>
>
1
2
3
4
5
6
7
id: 20230929102100
title: #test
role: tag
syntax: zmk
created: 20230929102125

Zettel with this tag are testing the Zettelstore.

Changes to tests/client/client_test.go.

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

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 47
		configRoleZettel = 29
		writerZettel     = ownerZettel - 23
		readerZettel     = ownerZettel - 23
		creatorZettel    = 7
		publicZettel     = 4
	)

	testdata := []struct {
		user string
		exp  int
	}{
		{"", publicZettel},







|
|
|
|
|
|







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

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 55
		configRoleZettel = 33
		writerZettel     = ownerZettel - 24
		readerZettel     = ownerZettel - 24
		creatorZettel    = 10
		publicZettel     = 5
	)

	testdata := []struct {
		user string
		exp  int
	}{
		{"", publicZettel},
211
212
213
214
215
216
217
218
219
220
221
222
223


224
225
226
227
228
229
230
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if got := len(metaSeq); got != 2 {
		t.Errorf("Expected list of length 2, got %d", got)
		return
	}
	checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel)
	checkListZid(t, metaSeq, 1, api.ZidTemplateNewUser)


}

// func TestGetZettelContext(t *testing.T) {
// 	const (
// 		allUserZid = api.ZettelID("20211019200500")
// 		ownerZid   = api.ZettelID("20210629163300")
// 		writerZid  = api.ZettelID("20210629165000")







|
|



|
>
>







211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if got := len(metaSeq); got != 4 {
		t.Errorf("Expected list of length 4, got %d", got)
		return
	}
	checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel)
	checkListZid(t, metaSeq, 1, api.ZidTemplateNewRole)
	checkListZid(t, metaSeq, 2, api.ZidTemplateNewTag)
	checkListZid(t, metaSeq, 3, api.ZidTemplateNewUser)
}

// func TestGetZettelContext(t *testing.T) {
// 	const (
// 		allUserZid = api.ZettelID("20211019200500")
// 		ownerZid   = api.ZettelID("20210629163300")
// 		writerZid  = api.ZettelID("20210629165000")
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if got := len(metaSeq); got != 1 {
		t.Errorf("Expected list of length 1, got %d", got)
		return
	}
}

func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool {
	if err != nil {
		if cErr, ok := err.(*client.Error); ok {







|







278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if got := len(metaSeq); got != 1 {
		t.Errorf("Expected list of length 1, got %d:\n%v", got, metaSeq)
		return
	}
}

func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool {
	if err != nil {
		if cErr, ok := err.(*client.Error); ok {
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
		size int
	}{
		{"#invisible", 1},
		{"#user", 4},
		{"#test", 4},
	}
	if len(agg) != len(tags) {
		t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(agg), agg)
	}
	for _, tag := range tags {
		if zl, ok := agg[tag.key]; !ok {
			t.Errorf("No tag %v: %v", tag.key, agg)
		} else if len(zl) != tag.size {
			t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
		}
	}
	for i, id := range agg["#user"] {
		if id != agg["#test"][i] {
			t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"])
		}
	}
}






















func TestListRoles(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole)
	if err != nil {
		t.Error(err)
		return
	}
	exp := []string{"configuration", "user", "zettel"}
	if len(agg) != len(exp) {
		t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(agg), agg)
	}
	for _, id := range exp {
		if _, found := agg[id]; !found {
			t.Errorf("Role map expected key %q", id)
		}
	}
}






















func TestVersion(t *testing.T) {
	t.Parallel()
	c := getClient()
	ver, err := c.GetVersionInfo(context.Background())
	if err != nil {
		t.Error(err)







|














>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>










|

|







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
		size int
	}{
		{"#invisible", 1},
		{"#user", 4},
		{"#test", 4},
	}
	if len(agg) != len(tags) {
		t.Errorf("Expected %d different tags, but got %d (%v)", len(tags), len(agg), agg)
	}
	for _, tag := range tags {
		if zl, ok := agg[tag.key]; !ok {
			t.Errorf("No tag %v: %v", tag.key, agg)
		} else if len(zl) != tag.size {
			t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
		}
	}
	for i, id := range agg["#user"] {
		if id != agg["#test"][i] {
			t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"])
		}
	}
}

func TestTagZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.AllowRedirect(true)
	c.SetAuth("owner", "owner")
	ctx := context.Background()
	zid, err := c.TagZettel(ctx, "nosuchtag")
	if err != nil {
		t.Error(err)
	} else if zid != "" {
		t.Errorf("no zid expected, but got %q", zid)
	}
	zid, err = c.TagZettel(ctx, "#test")
	exp := api.ZettelID("20230929102100")
	if err != nil {
		t.Error(err)
	} else if zid != exp {
		t.Errorf("tag zettel for #test should be %q, but got %q", exp, zid)
	}
}

func TestListRoles(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole)
	if err != nil {
		t.Error(err)
		return
	}
	exp := []string{"configuration", "role", "user", "tag", "zettel"}
	if len(agg) != len(exp) {
		t.Errorf("Expected %d different roles, but got %d (%v)", len(exp), len(agg), agg)
	}
	for _, id := range exp {
		if _, found := agg[id]; !found {
			t.Errorf("Role map expected key %q", id)
		}
	}
}

func TestRoleZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.AllowRedirect(true)
	c.SetAuth("owner", "owner")
	ctx := context.Background()
	zid, err := c.RoleZettel(ctx, "nosuchrole")
	if err != nil {
		t.Error("AAA", err)
	} else if zid != "" {
		t.Errorf("no zid expected, but got %q", zid)
	}
	zid, err = c.RoleZettel(ctx, "zettel")
	exp := api.ZettelID("00000000060010")
	if err != nil {
		t.Error(err)
	} else if zid != exp {
		t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid)
	}
}

func TestVersion(t *testing.T) {
	t.Parallel()
	c := getClient()
	ver, err := c.GetVersionInfo(context.Background())
	if err != nil {
		t.Error(err)

Changes to tests/markdown_test.go.

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	Section   string `json:"section"`
}

func TestEncoderAvailability(t *testing.T) {
	t.Parallel()
	encoderMissing := false
	for _, enc := range encodings {
		enc := encoder.Create(enc)
		if enc == nil {
			t.Errorf("No encoder for %q found", enc)
			encoderMissing = true
		}
	}
	if encoderMissing {
		panic("At least one encoder is missing. See test log")







|







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	Section   string `json:"section"`
}

func TestEncoderAvailability(t *testing.T) {
	t.Parallel()
	encoderMissing := false
	for _, enc := range encodings {
		enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		if enc == nil {
			t.Errorf("No encoder for %q found", enc)
			encoderMissing = true
		}
	}
	if encoderMissing {
		panic("At least one encoder is missing. See test log")
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk)
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()








|






|







82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, `abc@@<br>@@{="html"}def`},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk)
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		sb.Reset()
		zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		if got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
	}
}







|











126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, `abc@@<br>@@{="html"}def`},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		sb.Reset()
		zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		if got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
	}
}

Changes to tests/naughtystrings_test.go.

13
14
15
16
17
18
19

20
21
22
23
24
25
26
import (
	"bufio"
	"io"
	"os"
	"path/filepath"
	"testing"


	_ "zettelstore.de/z/cmd"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)








>







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import (
	"bufio"
	"io"
	"os"
	"path/filepath"
	"testing"

	"zettelstore.de/client.fossil/api"
	_ "zettelstore.de/z/cmd"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
		}
	}
	return result
}

func getAllEncoder() (result []encoder.Encoder) {
	for _, enc := range encoder.GetEncodings() {
		e := encoder.Create(enc)
		result = append(result, e)
	}
	return result
}

func TestNaughtyStringParser(t *testing.T) {
	blns, err := getNaughtyStrings()







|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
		}
	}
	return result
}

func getAllEncoder() (result []encoder.Encoder) {
	for _, enc := range encoder.GetEncodings() {
		e := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		result = append(result, e)
	}
	return result
}

func TestNaughtyStringParser(t *testing.T) {
	blns, err := getNaughtyStrings()

Changes to tests/regression_test.go.

25
26
27
28
29
30
31

32
33
34
35
36
37
38
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"

	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/box/dirbox"
)

var encodings = []api.EncodingEnum{
	api.EncoderHTML,







>







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/box/dirbox"
)

var encodings = []api.EncodingEnum{
	api.EncoderHTML,
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
	}
	return u.Path[len(root):]
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
	t.Helper()

	if enc := encoder.Create(enc); enc != nil {
		var sf strings.Builder
		enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, sf.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}

func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList := []*meta.Meta{}

	err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) }, nil)
	if err != nil {
		panic(err)
	}
	for _, meta := range metaList {
		zettel, err2 := p.GetZettel(context.Background(), meta.Zid)
		if err2 != nil {
			panic(err2)
		}
		z := parser.ParseZettel(context.Background(), zettel, "", testConfig)
		for _, enc := range encodings {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String())
				checkMetaFile(st, resultName, z, enc)
			})







|














>
|
|



|
|
|







117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
	}
	return u.Path[len(root):]
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
	t.Helper()

	if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}); enc != nil {
		var sf strings.Builder
		enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, sf.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}

func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList := []*meta.Meta{}
	if err := p.ApplyMeta(context.Background(),
		func(m *meta.Meta) { metaList = append(metaList, m) },
		query.AlwaysIncluded); err != nil {
		panic(err)
	}
	for _, meta := range metaList {
		zettel, err := p.GetZettel(context.Background(), meta.Zid)
		if err != nil {
			panic(err)
		}
		z := parser.ParseZettel(context.Background(), zettel, "", testConfig)
		for _, enc := range encodings {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String())
				checkMetaFile(st, resultName, z, enc)
			})

Changes to tools/build.go.

9
10
11
12
13
14
15

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


28


29
30
31
32
33
34
35
//-----------------------------------------------------------------------------

// Package main provides a command to build and run the software.
package main

import (
	"archive/zip"

	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"



	"zettelstore.de/z/strfun"


)

var envDirectProxy = []string{"GOPROXY=direct"}
var envGoVCS = []string{"GOVCS=zettelstore.de:fossil"}

func executeCommand(env []string, name string, arg ...string) (string, error) {
	logCommand("EXEC", env, name, arg)







>












>
>

>
>







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

// Package main provides a command to build and run the software.
package main

import (
	"archive/zip"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

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

var envDirectProxy = []string{"GOPROXY=direct"}
var envGoVCS = []string{"GOVCS=zettelstore.de:fossil"}

func executeCommand(env []string, name string, arg ...string) (string, error) {
	logCommand("EXEC", env, name, arg)
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
	for _, entry := range entries {
		if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
			return err
		}
	}
	return nil
}



func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error {
	info, err := entry.Info()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(info)
	if err != nil {
		return err
	}

	fh.Name = entry.Name()
	fh.Method = zip.Deflate
	w, err := zipWriter.CreateHeader(fh)
	if err != nil {
		return err
	}
	manualFile, err := os.Open(filepath.Join(path, entry.Name()))
	if err != nil {
		return err
	}
	defer manualFile.Close()


	_, err = io.Copy(w, manualFile)























	return err
}

func getReleaseVersionData() string {
	if fossil := getFossilDirty(); fossil != "" {
		fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version")
	}







>
>










>
|





|




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







378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
	for _, entry := range entries {
		if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
			return err
		}
	}
	return nil
}

const versionZid = "00001000000001"

func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error {
	info, err := entry.Info()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(info)
	if err != nil {
		return err
	}
	name := entry.Name()
	fh.Name = name
	fh.Method = zip.Deflate
	w, err := zipWriter.CreateHeader(fh)
	if err != nil {
		return err
	}
	manualFile, err := os.Open(filepath.Join(path, name))
	if err != nil {
		return err
	}
	defer manualFile.Close()

	if name != versionZid+".zettel" {
		_, err = io.Copy(w, manualFile)
		return err
	}

	data, err := io.ReadAll(manualFile)
	if err != nil {
		return err
	}
	inp := input.NewInput(data)
	m := meta.NewFromInput(id.MustParse(versionZid), inp)
	m.SetNow(api.KeyModified)

	var buf bytes.Buffer
	if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil {
		return err
	}
	if _, err = m.WriteComputed(&buf); err != nil {
		return err
	}
	version := getVersion()
	if _, err = fmt.Fprintf(&buf, "\n%s", version); err != nil {
		return err
	}
	_, err = io.Copy(w, &buf)
	return err
}

func getReleaseVersionData() string {
	if fossil := getFossilDirty(); fossil != "" {
		fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version")
	}

Changes to usecase/create_zettel.go.

42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
		rtConfig: rtConfig,
		port:     port,
	}
}

// PrepareCopy the zettel for further modification.
func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel {
	m := origZettel.Meta.Clone()

	if title, found := m.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of "))
	}
	setReadonly(m)
	content := origZettel.Content
	content.TrimSpace()
	return zettel.Zettel{Meta: m, Content: content}
}







|
>
|







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
		rtConfig: rtConfig,
		port:     port,
	}
}

// PrepareCopy the zettel for further modification.
func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := origMeta.Clone()
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of "))
	}
	setReadonly(m)
	content := origZettel.Content
	content.TrimSpace()
	return zettel.Zettel{Meta: m, Content: content}
}
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106



107
108
109
110
111
112
113
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareChild the zettel for further modification.
func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := origMeta.Clone()
	if title, found := m.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeySuperior, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel) zettel.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta
	m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	updateMetaRoleTagsSyntax(m, om)

	const prefixLen = len(meta.NewPrefix)
	for _, pair := range om.PairsRest() {
		if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
			m.Set(key[prefixLen:], pair.Value)
		}
	}



	content := origZettel.Content
	content.TrimSpace()
	return zettel.Zettel{Meta: m, Content: content}
}

func updateMetaRoleTagsSyntax(m, orig *meta.Meta) {
	m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, ""))







|
|








|











>
>
>







79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareChild the zettel for further modification.
func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeySuperior, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta
	m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	updateMetaRoleTagsSyntax(m, om)

	const prefixLen = len(meta.NewPrefix)
	for _, pair := range om.PairsRest() {
		if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
			m.Set(key[prefixLen:], pair.Value)
		}
	}
	if newTitle != "" {
		m.Set(api.KeyTitle, newTitle)
	}
	content := origZettel.Content
	content.TrimSpace()
	return zettel.Zettel{Meta: m, Content: content}
}

func updateMetaRoleTagsSyntax(m, orig *meta.Meta) {
	m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, ""))
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Run executes the use case.
func (uc *CreateZettel) Run(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	m := zettel.Meta
	if m.Zid.IsValid() {
		return m.Zid, nil // TODO: new error: already exists
	}

	m.Set(api.KeyCreated, time.Now().Local().Format(id.ZidLayout))
	m.Delete(api.KeyModified)
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content.TrimSpace()
	zid, err := uc.port.CreateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel")
	return zid, err
}







|








140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Run executes the use case.
func (uc *CreateZettel) Run(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	m := zettel.Meta
	if m.Zid.IsValid() {
		return m.Zid, nil // TODO: new error: already exists
	}

	m.Set(api.KeyCreated, time.Now().Local().Format(id.TimestampLayout))
	m.Delete(api.KeyModified)
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content.TrimSpace()
	zid, err := uc.port.CreateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel")
	return zid, err
}

Added usecase/get_special_zettel.go.





























































































































































































































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

package usecase

import (
	"context"

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

// TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that
// describes a given tag. A tag zettel must have the tag's name in its title
// and must have a role=tag.

// TagZettelPort is the interface used by this use case.
type TagZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// TagZettel is the data for this use case.
type TagZettel struct {
	port  GetZettelPort
	query *Query
}

// NewTagZettel creates a new use case.
func NewTagZettel(port GetZettelPort, query *Query) TagZettel {
	return TagZettel{port: port, query: query}
}

// Run executes the use case.
func (uc TagZettel) Run(ctx context.Context, tag string) (zettel.Zettel, error) {
	tag = meta.NormalizeTag(tag)
	q := query.Parse(
		api.KeyTitle + api.SearchOperatorEqual + tag + " " +
			api.KeyRole + api.SearchOperatorHas + api.ValueRoleTag)
	ml, err := uc.query.Run(ctx, q)
	if err != nil {
		return zettel.Zettel{}, err
	}
	for _, m := range ml {
		z, errZ := uc.port.GetZettel(ctx, m.Zid)
		if errZ == nil {
			return z, nil
		}
	}
	return zettel.Zettel{}, ErrTagZettelNotFound{Tag: tag}
}

// ErrTagZettelNotFound is returned if a tag zettel was not found.
type ErrTagZettelNotFound struct{ Tag string }

func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + etznf.Tag }

// RoleZettel is the usecase of retrieving a "role zettel", i.e. a zettel that
// describes a given role. A role zettel must have the role's name in its title
// and must have a role=role.

// RoleZettelPort is the interface used by this use case.
type RoleZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// RoleZettel is the data for this use case.
type RoleZettel struct {
	port  GetZettelPort
	query *Query
}

// NewRoleZettel creates a new use case.
func NewRoleZettel(port GetZettelPort, query *Query) RoleZettel {
	return RoleZettel{port: port, query: query}
}

// Run executes the use case.
func (uc RoleZettel) Run(ctx context.Context, role string) (zettel.Zettel, error) {
	q := query.Parse(
		api.KeyTitle + api.SearchOperatorEqual + role + " " +
			api.KeyRole + api.SearchOperatorHas + api.ValueRoleRole)
	ml, err := uc.query.Run(ctx, q)
	if err != nil {
		return zettel.Zettel{}, err
	}
	for _, m := range ml {
		z, errZ := uc.port.GetZettel(ctx, m.Zid)
		if errZ == nil {
			return z, nil
		}
	}
	return zettel.Zettel{}, ErrRoleZettelNotFound{Role: role}
}

// ErrRoleZettelNotFound is returned if a role zettel was not found.
type ErrRoleZettelNotFound struct{ Role string }

func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + etznf.Role }

Changes to usecase/query.go.

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
	candidates, err := uc.port.SelectMeta(ctx, nil, q)
	if err != nil {
		return nil
	}
	metaZids := id.NewSetCap(len(metaSeq))
	refZids := id.NewSetCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel
	for _, m := range metaSeq {
		metaZids.Zid(m.Zid)
		refZids.Zid(m.Zid)
		for _, pair := range m.ComputedPairsRest() {
			switch meta.Type(pair.Key) {
			case meta.TypeID:
				if zid, errParse := id.Parse(pair.Value); errParse == nil {
					refZids.Zid(zid)
				}
			case meta.TypeIDSet:
				for _, value := range meta.ListFromValue(pair.Value) {
					if zid, errParse := id.Parse(value); errParse == nil {
						refZids.Zid(zid)
					}
				}
			}
		}
	}
	candidates = filterByZid(candidates, refZids)
	return uc.filterCandidates(ctx, candidates, words)
}

func filterByZid(candidates []*meta.Meta, ignoreSeq id.Set) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
	for _, m := range candidates {
		if !ignoreSeq.Contains(m.Zid) {
			result = append(result, m)
		}
	}
	return result
}

func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {







|
|




|




|












|







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
	candidates, err := uc.port.SelectMeta(ctx, nil, q)
	if err != nil {
		return nil
	}
	metaZids := id.NewSetCap(len(metaSeq))
	refZids := id.NewSetCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel
	for _, m := range metaSeq {
		metaZids.Add(m.Zid)
		refZids.Add(m.Zid)
		for _, pair := range m.ComputedPairsRest() {
			switch meta.Type(pair.Key) {
			case meta.TypeID:
				if zid, errParse := id.Parse(pair.Value); errParse == nil {
					refZids.Add(zid)
				}
			case meta.TypeIDSet:
				for _, value := range meta.ListFromValue(pair.Value) {
					if zid, errParse := id.Parse(value); errParse == nil {
						refZids.Add(zid)
					}
				}
			}
		}
	}
	candidates = filterByZid(candidates, refZids)
	return uc.filterCandidates(ctx, candidates, words)
}

func filterByZid(candidates []*meta.Meta, ignoreSeq id.Set) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
	for _, m := range candidates {
		if !ignoreSeq.ContainsOrNil(m.Zid) {
			result = append(result, m)
		}
	}
	return result
}

func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {

Added usecase/reindex.go.



















































































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

package usecase

import (
	"context"

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

// ReIndexPort is the interface used by this use case.
type ReIndexPort interface {
	ReIndex(context.Context, id.Zid) error
}

// ReIndex is the data for this use case.
type ReIndex struct {
	log  *logger.Logger
	port ReIndexPort
}

// NewReIndex creates a new use case.
func NewReIndex(log *logger.Logger, port ReIndexPort) ReIndex {
	return ReIndex{log: log, port: port}
}

// Run executes the use case.
func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error {
	err := uc.port.ReIndex(ctx, zid)
	uc.log.Sense().User(ctx).Err(err).Zid(zid).Msg("ReIndex zettel")
	return err
}

Changes to usecase/rename_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
	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
}







|



















|





30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	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.

68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
func (a *API) getToken(ident *meta.Meta) ([]byte, error) {
	return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI)
}

func (a *API) reportUsecaseError(w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		a.log.IfErr(err).Msg(text)
		http.Error(w, http.StatusText(code), code)
		return
	}
	// TODO: must call PrepareHeader somehow
	http.Error(w, text, code)
}








|







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
func (a *API) getToken(ident *meta.Meta) ([]byte, error) {
	return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI)
}

func (a *API) reportUsecaseError(w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		a.log.Error().Err(err).Msg(text)
		http.Error(w, http.StatusText(code), code)
		return
	}
	// TODO: must call PrepareHeader somehow
	http.Error(w, text, code)
}

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

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

package api

import (
	"bytes"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

type zidJSON struct {
	ID api.ZettelID `json:"id"`
}

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		enc, encStr := getEncoding(r, q)
		var zettel zettel.Zettel
		var err error
		switch enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, id.Invalid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, id.Invalid)
		case api.EncoderJson:
			zettel, err = buildZettelFromJSONData(r, id.Invalid)
		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		if err != nil {
			a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return













<











<
<
<
<













<
<







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

14
15
16
17
18
19
20
21
22
23
24




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


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

package api

import (

	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)





// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		enc, encStr := getEncoding(r, q)
		var zettel zettel.Zettel
		var err error
		switch enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, id.Invalid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, id.Invalid)


		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		if err != nil {
			a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

		switch enc {
		case api.EncoderPlain:
			result = newZid.Bytes()
			contentType = content.PlainText
		case api.EncoderData:
			result = []byte(sx.Int64(newZid).Repr())
			contentType = content.SXPF
		case api.EncoderJson:
			var buf bytes.Buffer
			err = encodeJSONData(&buf, zidJSON{ID: api.ZettelID(newZid.String())})
			if err != nil {
				a.log.Fatal().Err(err).Zid(newZid).Msg("Unable to store new Zid in buffer")
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
				return
			}
			result = buf.Bytes()
			contentType = content.JSON
			location.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson)
		default:
			panic(encStr)
		}

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		_, err = w.Write(result)
		a.log.IfErr(err).Zid(newZid).Msg("Create Zettel")
	}
}








<
<
<
<
<
<
<
<
<
<
<







|
|
|
|
>
57
58
59
60
61
62
63











64
65
66
67
68
69
70
71
72
73
74
75
		switch enc {
		case api.EncoderPlain:
			result = newZid.Bytes()
			contentType = content.PlainText
		case api.EncoderData:
			result = []byte(sx.Int64(newZid).Repr())
			contentType = content.SXPF











		default:
			panic(encStr)
		}

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		if _, err = w.Write(result); err != nil {
			a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel")
		}
	}
}

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

22
23
24
25
26
27
28
29
30
31

32
33
34

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

		a.log.IfErr(err).Msg("Write Version Info")
	}
}








|
|

>
|
|
|
>
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		version := ucVersion.Run()
		err := a.writeObject(w, id.Invalid, sx.MakeList(
			sx.Int64(version.Major),
			sx.Int64(version.Minor),
			sx.Int64(version.Patch),
			sx.String(version.Info),
			sx.String(version.Hash),
		))
		if err != nil {
			a.log.Error().Err(err).Msg("Write Version Info")
		}
	}
}

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

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
		switch enc, encStr := getEncoding(r, q); enc {
		case api.EncoderPlain:
			a.writePlainData(w, ctx, zid, part, getZettel)

		case api.EncoderData:
			a.writeSzData(w, ctx, zid, part, getZettel)

		case api.EncoderJson:
			a.writeJSONData(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(w, zn, em, enc, encStr, part)
		}
	}
}

func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var contentType string







<
<
<
















|







44
45
46
47
48
49
50



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
		switch enc, encStr := getEncoding(r, q); enc {
		case api.EncoderPlain:
			a.writePlainData(w, ctx, zid, part, getZettel)

		case api.EncoderData:
			a.writeSzData(w, ctx, zid, part, getZettel)




		default:
			var zn *ast.ZettelNode
			var em func(value string) ast.InlineSlice
			if q.Has(api.QueryKeyParseOnly) {
				zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
				em = parser.ParseMetadata
			} else {
				zn, err = evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
				em = func(value string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, value)
				}
			}
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
			a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part)
		}
	}
}

func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var contentType string
103
104
105
106
107
108
109
110
111

112
113
114
115
116
117
118
	}

	if err != nil {
		a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	err = writeBuffer(w, &buf, contentType)
	a.log.IfErr(err).Zid(zid).Msg("Write Plain data")

}

func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)
		return







|
|
>







100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
	}

	if err != nil {
		a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if err = writeBuffer(w, &buf, contentType); err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("Write Plain data")
	}
}

func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)
		return
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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

	case partMeta:
		obj = sexp.EncodeMetaRights(api.MetaRights{
			Meta:   z.Meta.Map(),
			Rights: a.getRights(ctx, z.Meta),
		})
	}
	err = a.writeObject(w, zid, obj)
	a.log.IfErr(err).Zid(zid).Msg("write sx data")
}

type zettelJSON struct {
	ID       api.ZettelID     `json:"id"`
	Meta     api.ZettelMeta   `json:"meta"`
	Encoding string           `json:"encoding"`
	Content  string           `json:"content"`
	Rights   api.ZettelRights `json:"rights"`
}
type zettelMetaJSON struct {
	Meta   api.ZettelMeta   `json:"meta"`
	Rights api.ZettelRights `json:"rights"`
}
type zettelContentJSON struct {
	Encoding string `json:"encoding"`
	Content  string `json:"content"`
}

func (a *API) writeJSONData(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 buf bytes.Buffer
	switch part {
	case partZettel:
		zContent, encoding := z.Content.Encode()
		err = encodeJSONData(&buf, zettelJSON{
			ID:       api.ZettelID(zid.String()),
			Meta:     z.Meta.Map(),
			Encoding: encoding,
			Content:  zContent,
			Rights:   a.getRights(ctx, z.Meta),
		})

	case partMeta:
		m := z.Meta
		err = encodeJSONData(&buf, zettelMetaJSON{
			Meta:   m.Map(),
			Rights: a.getRights(ctx, m),
		})

	case partContent:
		zContent, encoding := z.Content.Encode()
		err = encodeJSONData(&buf, zettelContentJSON{
			Encoding: encoding,
			Content:  zContent,
		})
	}
	if err != nil {
		a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store zettel in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	err = writeBuffer(w, &buf, content.JSON)
	a.log.IfErr(err).Zid(zid).Msg("Write JSON data")
}

func (a *API) writeEncodedZettelPart(

	w http.ResponseWriter, zn *ast.ZettelNode,
	evalMeta encoder.EvalMetaFunc,
	enc api.EncodingEnum, encStr string, part partType,
) {
	encdr := encoder.Create(enc)




	if encdr == nil {
		adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr))
		return
	}
	var err error
	var buf bytes.Buffer
	switch part {







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

>




|
>
>
>
>







128
129
130
131
132
133
134
135
136
137
138






139



















































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

	case partMeta:
		obj = sexp.EncodeMetaRights(api.MetaRights{
			Meta:   z.Meta.Map(),
			Rights: a.getRights(ctx, z.Meta),
		})
	}
	if err = a.writeObject(w, zid, obj); err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("write sx data")
	}
}


























































func (a *API) writeEncodedZettelPart(
	ctx context.Context,
	w http.ResponseWriter, zn *ast.ZettelNode,
	evalMeta encoder.EvalMetaFunc,
	enc api.EncodingEnum, encStr string, part partType,
) {
	encdr := encoder.Create(
		enc,
		&encoder.CreateParameter{
			Lang: a.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang),
		})
	if encdr == nil {
		adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr))
		return
	}
	var err error
	var buf bytes.Buffer
	switch part {
222
223
224
225
226
227
228
229
230
231

		return
	}
	if buf.Len() == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc))
	a.log.IfErr(err).Zid(zn.Zid).Msg("Write Encoded Zettel")
}








|
|
|
>
168
169
170
171
172
173
174
175
176
177
178
		return
	}
	if buf.Len() == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil {
		a.log.Error().Err(err).Zid(zn.Zid).Msg("Write Encoded Zettel")
	}
}

Deleted web/adapter/api/json.go.

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

package api

import (
	"encoding/json"
	"io"
	"net/http"

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

func encodeJSONData(w io.Writer, data interface{}) error {
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)
	return enc.Encode(data)
}

type zettelDataJSON struct {
	Meta     api.ZettelMeta `json:"meta"`
	Encoding string         `json:"encoding"`
	Content  string         `json:"content"`
}

func buildZettelFromJSONData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	var zettel zettel.Zettel
	defer r.Body.Close()
	dec := json.NewDecoder(r.Body)
	var zettelData zettelDataJSON
	if err := dec.Decode(&zettelData); err != nil {
		return zettel, err
	}
	m := meta.New(zid)
	for k, v := range zettelData.Meta {
		m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
	}
	zettel.Meta = m
	if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil {
		return zettel, err
	}
	return zettel, nil
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































































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

21
22
23
24
25
26
27
28
29

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

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

69
70
71
72
73
74
75
76
77
78
79
80
81

82
83
84
85
86
87
88
89
90
91
92

93
94
95
96
97
98
99
100
101
102
	"zettelstore.de/z/zettel/id"
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour)
			a.log.IfErr(err).Msg("Login/free")

			return
		}
		var token []byte
		if ident, cred := retrieveIdentCred(r); ident != "" {
			var err error
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
		}
		if len(token) == 0 {
			w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		err := a.writeToken(w, string(token), a.tokenLifetime)
		a.log.IfErr(err).Msg("Login")

	}
}

func retrieveIdentCred(r *http.Request) (string, string) {
	if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok {
		return ident, cred
	}
	if ident, cred, ok := r.BasicAuth(); ok {
		return ident, cred
	}
	return "", ""
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if !a.withAuth() {
			err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour)
			a.log.IfErr(err).Msg("Refresh/free")

			return
		}
		authData := a.getAuthData(ctx)
		if authData == nil || len(authData.Token) == 0 || authData.User == nil {
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := authData.Expires.Sub(authData.Issued)
		currentLifetime := authData.Now.Sub(authData.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime)
			a.log.IfErr(err).Msg("Write old token")

			return
		}

		// Token is a little bit aged. Create a new one
		token, err := a.getToken(authData.User)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		err = a.writeToken(w, string(token), a.tokenLifetime)
		a.log.IfErr(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)),
	))
}







|
|
>

















|
|
>


















|
|
>











|
|
>









|
|
>





|
|



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
	"zettelstore.de/z/zettel/id"
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Login/free")
			}
			return
		}
		var token []byte
		if ident, cred := retrieveIdentCred(r); ident != "" {
			var err error
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
		}
		if len(token) == 0 {
			w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Login")
		}
	}
}

func retrieveIdentCred(r *http.Request) (string, string) {
	if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok {
		return ident, cred
	}
	if ident, cred, ok := r.BasicAuth(); ok {
		return ident, cred
	}
	return "", ""
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Refresh/free")
			}
			return
		}
		authData := a.getAuthData(ctx)
		if authData == nil || len(authData.Token) == 0 || authData.User == nil {
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := authData.Expires.Sub(authData.Issued)
		currentLifetime := authData.Now.Sub(authData.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil {
				a.log.Error().Err(err).Msg("Write old token")
			}
			return
		}

		// Token is a little bit aged. Create a new one
		token, err := a.getToken(authData.User)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Write renewed token")
		}
	}
}

func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
	return a.writeObject(w, id.Invalid, sx.MakeList(
		sx.String("Bearer"),
		sx.String(token),
		sx.Int64(int64(lifetime/time.Second)),
	))
}

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

11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35







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

82
83
84
85
86
87
88
89
package api

import (
	"bytes"
	"fmt"
	"io"
	"net/http"

	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"

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

// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(queryMeta *usecase.Query) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()







		sq := adapter.GetQuery(q)

		metaSeq, err := queryMeta.Run(ctx, sq)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		var encoder zettelEncoder
		var contentType string
		switch enc, _ := getEncoding(r, q); enc {
		case api.EncoderPlain:
			encoder = &plainZettelEncoder{}
			contentType = content.PlainText

		case api.EncoderData:
			encoder = &dataZettelEncoder{
				sf:        sx.MakeMappedFactory(),
				sq:        sq,
				getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) },
			}
			contentType = content.SXPF

		case api.EncoderJson: // DEPRECATED
			encoder = &jsonZettelEncoder{
				sq:        sq,
				getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) },
			}
			contentType = content.JSON

		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		var buf bytes.Buffer
		err = queryAction(&buf, encoder, metaSeq, sq)
		if err != nil {
			a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}

		err = writeBuffer(w, &buf, contentType)
		a.log.IfErr(err).Msg("write result buffer")
	}
}

func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, sq *query.Query) error {
	min, max := -1, -1
	if actions := sq.Actions(); len(actions) > 0 {
		acts := make([]string, 0, len(actions))
		for _, act := range actions {
			if strings.HasPrefix(act, "MIN") {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					min = num







>










>




|


|
>
>
>
>
>
>
>
|









|






|





<
<
<
<
<
<
<






|





|
|
|
|
>
|







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







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package api

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/sexp"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		urlQuery := r.URL.Query()
		if a.handleTagZettel(w, r, tagZettel, urlQuery) {
			return
		}
		if a.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}

		sq := adapter.GetQuery(urlQuery)

		metaSeq, err := queryMeta.Run(ctx, sq)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		var encoder zettelEncoder
		var contentType string
		switch enc, _ := getEncoding(r, urlQuery); enc {
		case api.EncoderPlain:
			encoder = &plainZettelEncoder{}
			contentType = content.PlainText

		case api.EncoderData:
			encoder = &dataZettelEncoder{
				sf:        sx.MakeMappedFactory(256),
				sq:        sq,
				getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) },
			}
			contentType = content.SXPF








		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		var buf bytes.Buffer
		err = queryAction(&buf, encoder, metaSeq, sq, func(zid id.Zid) error { return reIndex.Run(ctx, zid) })
		if err != nil {
			a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}

		if err = writeBuffer(w, &buf, contentType); err != nil {
			a.log.Error().Err(err).Msg("write result buffer")
		}
	}
}
func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, sq *query.Query, reindex func(id.Zid) error) error {
	min, max := -1, -1
	if actions := sq.Actions(); len(actions) > 0 {
		acts := make([]string, 0, len(actions))
		for _, act := range actions {
			if strings.HasPrefix(act, "MIN") {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					min = num
98
99
100
101
102
103
104






105
106
107
108
109
110
111
			}
			acts = append(acts, act)
		}
		for _, act := range acts {
			switch act {
			case "KEYS":
				return encodeKeysArrangement(w, enc, ml, act)






			}
			switch key := strings.ToLower(act); meta.Type(key) {
			case meta.TypeWord, meta.TypeTagSet:
				return encodeMetaKeyArrangement(w, enc, ml, key, min, max)
			}
		}
	}







>
>
>
>
>
>







101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
			}
			acts = append(acts, act)
		}
		for _, act := range acts {
			switch act {
			case "KEYS":
				return encodeKeysArrangement(w, enc, ml, act)
			case "REINDEX":
				for _, m := range ml {
					if err := reindex(m.Zid); err != nil {
						return err
					}
				}
			}
			switch key := strings.ToLower(act); meta.Type(key) {
			case meta.TypeWord, meta.TypeTagSet:
				return encodeMetaKeyArrangement(w, enc, ml, key, min, max)
			}
		}
	}
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
		})
		msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel)
		result[i+1] = msz
	}

	_, err := sx.Print(w, sx.MakeList(
		sf.MustMake("meta-list"),
		sx.MakeList(sf.MustMake("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sf.MustMake("human"), sx.MakeString(dze.sq.Human())),
		sx.MakeList(result...),
	))
	return err
}
func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
	sf := dze.sf
	result := sx.Nil()
	for aggKey, metaList := range arr {
		sxMeta := sx.Nil()
		for i := len(metaList) - 1; i >= 0; i-- {
			sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
		}
		sxMeta = sxMeta.Cons(sx.MakeString(aggKey))
		result = result.Cons(sxMeta)
	}
	_, err := sx.Print(w, sx.MakeList(
		sf.MustMake("aggregate"),
		sx.MakeString(act),
		sx.MakeList(sf.MustMake("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sf.MustMake("human"), sx.MakeString(dze.sq.Human())),
		result.Cons(sf.MustMake("list")),
	))
	return err
}

// jsonZettelEncoder is DEPRECATED
type jsonZettelEncoder struct {
	sq        *query.Query

	getRights func(*meta.Meta) api.ZettelRights
}






type zidMetaJSON struct {

	ID     api.ZettelID     `json:"id"`

	Meta   api.ZettelMeta   `json:"meta"`
	Rights api.ZettelRights `json:"rights"`

}



type zettelListJSON struct {

	Query string        `json:"query"`
	Human string        `json:"human"`
	List  []zidMetaJSON `json:"list"`

}


func (jze *jsonZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
	result := make([]zidMetaJSON, 0, len(ml))
	for _, m := range ml {
		result = append(result, zidMetaJSON{
			ID:     api.ZettelID(m.Zid.String()),
			Meta:   m.Map(),
			Rights: jze.getRights(m),
		})
	}






	err := encodeJSONData(w, zettelListJSON{
		Query: jze.sq.String(),
		Human: jze.sq.Human(),
		List:  result,
	})
	return err
}

type mapListJSON struct {



	Map api.Aggregate `json:"map"`

}



func (*jsonZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error {
	mm := make(api.Aggregate, len(arr))
	for key, metaList := range arr {
		zidList := make([]api.ZettelID, 0, len(metaList))
		for _, m := range metaList {
			zidList = append(zidList, api.ZettelID(m.Zid.String()))
		}
		mm[key] = zidList



	}
	return encodeJSONData(w, mapListJSON{Map: mm})
}







|
|












|




|
|
|





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

|

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
		})
		msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel)
		result[i+1] = msz
	}

	_, err := sx.Print(w, sx.MakeList(
		sf.MustMake("meta-list"),
		sx.MakeList(sf.MustMake("query"), sx.String(dze.sq.String())),
		sx.MakeList(sf.MustMake("human"), sx.String(dze.sq.Human())),
		sx.MakeList(result...),
	))
	return err
}
func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
	sf := dze.sf
	result := sx.Nil()
	for aggKey, metaList := range arr {
		sxMeta := sx.Nil()
		for i := len(metaList) - 1; i >= 0; i-- {
			sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
		}
		sxMeta = sxMeta.Cons(sx.String(aggKey))
		result = result.Cons(sxMeta)
	}
	_, err := sx.Print(w, sx.MakeList(
		sf.MustMake("aggregate"),
		sx.String(act),
		sx.MakeList(sf.MustMake("query"), sx.String(dze.sq.String())),
		sx.MakeList(sf.MustMake("human"), sx.String(dze.sq.Human())),
		result.Cons(sf.MustMake("list")),
	))
	return err
}

func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {

	tag := vals.Get(api.QueryKeyTag)
	if tag == "" {
		return false
	}
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, tag)
	if err != nil {
		a.reportUsecaseError(w, err)
		return true
	}
	zid := z.Meta.Zid.String()
	w.Header().Set(api.HeaderContentType, content.PlainText)
	newURL := a.NewURLBuilder('z').SetZid(api.ZettelID(zid))
	for key, slVals := range vals {
		if key == api.QueryKeyTag {

			continue
		}
		for _, val := range slVals {
			newURL.AppendKVQuery(key, val)
		}

	}
	http.Redirect(w, r, newURL.String(), http.StatusFound)
	if _, err = io.WriteString(w, zid); err != nil {

		a.log.Error().Err(err).Msg("redirect body")
	}
	return true
}









func (a *API) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool {
	role := vals.Get(api.QueryKeyRole)
	if role == "" {
		return false
	}
	ctx := r.Context()
	z, err := roleZettel.Run(ctx, role)
	if err != nil {

		a.reportUsecaseError(w, err)

		return true
	}
	zid := z.Meta.Zid.String()

	w.Header().Set(api.HeaderContentType, content.PlainText)
	newURL := a.NewURLBuilder('z').SetZid(api.ZettelID(zid))
	for key, slVals := range vals {
		if key == api.QueryKeyRole {
			continue
		}
		for _, val := range slVals {
			newURL.AppendKVQuery(key, val)
		}






	}

	http.Redirect(w, r, newURL.String(), http.StatusFound)
	if _, err = io.WriteString(w, zid); err != nil {
		a.log.Error().Err(err).Msg("redirect body")
	}
	return true
}

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

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
			}
		}
	}
	return "", false
}

var mapCT2encoding = map[string]string{
	"application/json": "json",
	"text/html":        api.EncodingHTML,
}

func contentType2encoding(contentType string) (string, bool) {
	// TODO: only check before first ';'
	enc, ok := mapCT2encoding[contentType]
	return enc, ok
}







<
|







47
48
49
50
51
52
53

54
55
56
57
58
59
60
61
			}
		}
	}
	return "", false
}

var mapCT2encoding = map[string]string{

	"text/html": api.EncodingHTML,
}

func contentType2encoding(contentType string) (string, bool) {
	// TODO: only check before first ';'
	enc, ok := mapCT2encoding[contentType]
	return enc, ok
}

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

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
		q := r.URL.Query()
		var zettel zettel.Zettel
		switch enc, _ := getEncoding(r, q); enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, zid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, zid)
		case api.EncoderJson:
			zettel, err = buildZettelFromJSONData(r, zid)
		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		if err != nil {
			a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))







<
<







32
33
34
35
36
37
38


39
40
41
42
43
44
45
		q := r.URL.Query()
		var zettel zettel.Zettel
		switch enc, _ := getEncoding(r, q); enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, zid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, zid)


		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		if err != nil {
			a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))

Changes to web/adapter/errors.go.

23
24
25
26
27
28
29





	http.Error(w, text, http.StatusForbidden)
}

// NotFound signals HTTP status code 404.
func NotFound(w http.ResponseWriter, text string) {
	http.Error(w, text, http.StatusNotFound)
}












>
>
>
>
>
23
24
25
26
27
28
29
30
31
32
33
34
	http.Error(w, text, http.StatusForbidden)
}

// NotFound signals HTTP status code 404.
func NotFound(w http.ResponseWriter, text string) {
	http.Error(w, text, http.StatusNotFound)
}

// ErrResourceNotFound is signalled when a web resource was not found.
type ErrResourceNotFound struct{ Path string }

func (ernf ErrResourceNotFound) Error() string { return "resource not found: " + ernf.Path }

Changes to web/adapter/response.go.

10
11
12
13
14
15
16

17
18
19
20
21
22
23

package adapter

import (
	"errors"
	"fmt"
	"net/http"


	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// WriteData emits the given data to the response writer.







>







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

package adapter

import (
	"errors"
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// WriteData emits the given data to the response writer.
43
44
45
46
47
48
49
50
51
52
53
54
55
56

57
58
59


60
61
62

63
64
65

66
67








68

69
70
71
72
73
74
75
76
77
78
79




80
81

// ErrBadRequest is returned if the caller made an invalid HTTP request.
type ErrBadRequest struct {
	Text string
}

// NewErrBadRequest creates an new bad request error.
func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} }

func (err *ErrBadRequest) Error() string { return err.Text }

// CodeMessageFromError returns an appropriate HTTP status code and text from a given error.
func CodeMessageFromError(err error) (int, string) {
	if err == box.ErrNotFound {

		return http.StatusNotFound, http.StatusText(http.StatusNotFound)
	}
	if err1, ok := err.(*box.ErrNotAllowed); ok {


		return http.StatusForbidden, err1.Error()
	}
	if err1, ok := err.(*box.ErrInvalidID); ok {

		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid)
	}
	if err1, ok := err.(*usecase.ErrZidInUse); ok {

		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid)
	}








	if err1, ok := err.(*ErrBadRequest); ok {

		return http.StatusBadRequest, err1.Text
	}
	if errors.Is(err, box.ErrStopped) {
		return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err)
	}
	if errors.Is(err, box.ErrConflict) {
		return http.StatusConflict, "Zettelstore operations conflicted"
	}
	if errors.Is(err, box.ErrCapacity) {
		return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits"
	}




	return http.StatusInternalServerError, err.Error()
}







|

|



|
>
|

|
>
>
|

|
>
|

|
>
|

>
>
>
>
>
>
>
>
|
>
|










>
>
>
>


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

// ErrBadRequest is returned if the caller made an invalid HTTP request.
type ErrBadRequest struct {
	Text string
}

// NewErrBadRequest creates an new bad request error.
func NewErrBadRequest(text string) error { return ErrBadRequest{Text: text} }

func (err ErrBadRequest) Error() string { return err.Text }

// CodeMessageFromError returns an appropriate HTTP status code and text from a given error.
func CodeMessageFromError(err error) (int, string) {
	var eznf box.ErrZettelNotFound
	if errors.As(err, &eznf) {
		return http.StatusNotFound, "Zettel not found: " + eznf.Zid.String()
	}
	var ena *box.ErrNotAllowed
	if errors.As(err, &ena) {
		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
	}
	var ebr ErrBadRequest
	if errors.As(err, &ebr) {
		return http.StatusBadRequest, ebr.Text
	}
	if errors.Is(err, box.ErrStopped) {
		return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err)
	}
	if errors.Is(err, box.ErrConflict) {
		return http.StatusConflict, "Zettelstore operations conflicted"
	}
	if errors.Is(err, box.ErrCapacity) {
		return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits"
	}
	var ernf ErrResourceNotFound
	if errors.As(err, &ernf) {
		return http.StatusNotFound, "Resource not found: " + ernf.Path
	}
	return http.StatusInternalServerError, err.Error()
}

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

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// under this license.
//-----------------------------------------------------------------------------

package webui

// WebUI related constants.

const queryKeyAction = "action"

// Values for queryKeyAction
const (
	valueActionChild   = "child"
	valueActionCopy    = "copy"
	valueActionFolge   = "folge"
	valueActionNew     = "new"







|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// under this license.
//-----------------------------------------------------------------------------

package webui

// WebUI related constants.

const queryKeyAction = "_action"

// Values for queryKeyAction
const (
	valueActionChild   = "child"
	valueActionCopy    = "copy"
	valueActionFolge   = "folge"
	valueActionNew     = "new"

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

35
36
37
38
39
40
41

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

63
64
65
66
67
68
69
70
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel, createZettel *usecase.CreateZettel,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		op := getCreateAction(q.Get(queryKeyAction))

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}
		origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			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())

			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), title, "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		}
	}
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {







>
|

|




|













>
|







35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel, createZettel *usecase.CreateZettel,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		op := getCreateAction(q.Get(queryKeyAction))
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}
		origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid})
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {
		case actionChild:
			wui.renderZettelForm(ctx, w, createZettel.PrepareChild(origZettel), "Child Zettel", "", roleData, syntaxData)
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle))
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		}
	}
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
	for _, p := range m.PairsRest() {
		sb.WriteString(p.Key)
		sb.WriteString(": ")
		sb.WriteString(p.Value)
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
	rb.bindString("heading", sx.MakeString(title))
	rb.bindString("form-action-url", sx.MakeString(formActionURL))
	rb.bindString("role-data", makeStringList(roleData))
	rb.bindString("syntax-data", makeStringList(syntaxData))
	rb.bindString("meta", sx.MakeString(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.MakeString(ztl.Content.AsString()))
	}
	wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content)
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)







|
|


|

|







100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
	for _, p := range m.PairsRest() {
		sb.WriteString(p.Key)
		sb.WriteString(": ")
		sb.WriteString(p.Value)
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
	rb.bindString("heading", sx.String(title))
	rb.bindString("form-action-url", sx.String(formActionURL))
	rb.bindString("role-data", makeStringList(roleData))
	rb.bindString("syntax-data", makeStringList(syntaxData))
	rb.bindString("meta", sx.String(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.String(ztl.Content.AsString()))
	}
	wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content)
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59


60
61
62
63
64
65
66
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "delete",
			wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.MakeString(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???")))
			rb.bindString("incoming", nil)
		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		}
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)

		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)


		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}








>
|

|















|









>
>







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

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "delete",
			wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.String(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???")))
			rb.bindString("incoming", nil)
		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		}
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)

		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

93
94
95
96
97
98
99

100
101
102
103
104
105
106
107
108
109
110
111
112
	}
}

// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}







>
|

|










96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
	}
}

// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}

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

21
22
23
24
25
26
27

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

50
51
52
53
54
55
56
57
58
59
)

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData)
	}
}

// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		reEdit, zettel, err := parseZettelForm(r, zid)
		hasContent := true
		if err != nil {
			if err != errMissingContent {







>
|

|



















>
|

|







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

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData)
	}
}

// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		reEdit, zettel, err := parseZettelForm(r, zid)
		hasContent := true
		if err != nil {
			if err != errMissingContent {

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

33
34
35
36
37
38
39
40
41
42
43

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

		err = adapter.WriteData(w, data, "")
		wui.log.IfErr(err).Msg("Write favicon")
	}
}








|
|
|
|
>
33
34
35
36
37
38
39
40
41
42
43
44
		data, err := io.ReadAll(f)
		if err != nil {
			wui.log.Info().Err(err).Msg("Unable to read favicon data")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}

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

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

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
	}
	if postTitle, ok := trimmedFormValue(r, "title"); ok {
		m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
	}
	if postTags, ok := trimmedFormValue(r, "tags"); ok {
		if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
			for i, tag := range tags {
				if tag[0] != '#' {
					tags[i] = "#" + tag
				}
			}
			m.SetList(api.KeyTags, tags)
		}
	}
	if postRole, ok := trimmedFormValue(r, "role"); ok {
		m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole))
	}







<
|
<







53
54
55
56
57
58
59

60

61
62
63
64
65
66
67
	}
	if postTitle, ok := trimmedFormValue(r, "title"); ok {
		m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
	}
	if postTags, ok := trimmedFormValue(r, "tags"); ok {
		if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
			for i, tag := range tags {

				tags[i] = meta.NormalizeTag(tag)

			}
			m.SetList(api.KeyTags, tags)
		}
	}
	if postRole, ok := trimmedFormValue(r, "role"); ok {
		m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole))
	}

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

39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
	ucGetAllMeta usecase.GetAllZettel,
	ucQuery *usecase.Query,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()


		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder()
		getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
		evalMeta := func(val string) ast.InlineSlice {
			return ucEvaluate.RunMetadata(ctx, val)
		}
		pairs := zn.Meta.ComputedPairs()
		metadata := sx.Nil()
		for i := len(pairs) - 1; i >= 0; i-- {
			key := pairs[i].Key
			sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
			metadata = metadata.Cons(sx.Cons(sx.MakeString(key), sxval))
		}

		summary := collect.References(zn)
		locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)







>
|

|









|









|







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
	ucGetAllMeta usecase.GetAllZettel,
	ucQuery *usecase.Query,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()

		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
		getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
		evalMeta := func(val string) ast.InlineSlice {
			return ucEvaluate.RunMetadata(ctx, val)
		}
		pairs := zn.Meta.ComputedPairs()
		metadata := sx.Nil()
		for i := len(pairs) - 1; i >= 0; i-- {
			key := pairs[i].Key
			sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
			metadata = metadata.Cons(sx.Cons(sx.String(key), sxval))
		}

		summary := collect.References(zn)
		locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
		rb.bindString("metadata", metadata)
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.MakeString(phrase))
		rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env)


		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) {
	for i := len(links) - 1; i >= 0; i-- {
		ref := links[i]
		if ref.State == ast.RefStateSelf || ref.IsZettel() {
			continue
		}
		if ref.State == ast.RefStateQuery {
			queries = queries.Cons(
				sx.Cons(
					sx.MakeString(ref.Value),
					sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))
			continue
		}
		if ref.IsExternal() {
			extLinks = extLinks.Cons(sx.MakeString(ref.String()))
			continue
		}
		locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.MakeString(ref.String())))
	}
	return locLinks, queries, extLinks
}

func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
	var sb strings.Builder
	sb.Write(zid.Bytes())







|
|






>
>
















|
|



|


|







95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
		rb.bindString("metadata", metadata)
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.String(phrase))
		rb.bindString("query-key-phrase", sx.String(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) {
	for i := len(links) - 1; i >= 0; i-- {
		ref := links[i]
		if ref.State == ast.RefStateSelf || ref.IsZettel() {
			continue
		}
		if ref.State == ast.RefStateQuery {
			queries = queries.Cons(
				sx.Cons(
					sx.String(ref.Value),
					sx.String(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))
			continue
		}
		if ref.IsExternal() {
			extLinks = extLinks.Cons(sx.String(ref.String()))
			continue
		}
		locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.String(ref.String())))
	}
	return locLinks, queries, extLinks
}

func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
	var sb strings.Builder
	sb.Write(zid.Bytes())
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
		for je := len(encTexts) - 1; je >= 0; je-- {
			enc := encTexts[je]
			if parseOnly {
				u.AppendKVQuery(api.QueryKeyParseOnly, "")
			}
			u.AppendKVQuery(api.QueryKeyPart, part)
			u.AppendKVQuery(api.QueryKeyEncoding, enc)
			row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String())))
			u.ClearQuery()
		}
		matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row))
	}
	return matrix
}

func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair {
	matrix := wui.infoAPIMatrix(zid, true, encTexts)
	u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))

	for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() {
		line, isLine := sx.GetPair(row.Car())
		if !isLine || line == nil {
			continue
		}
		last := line.LastPair()
		part := apiParts[i]
		u.AppendKVQuery(api.QueryKeyPart, part)
		last = last.AppendBang(sx.Cons(sx.MakeString("plain"), sx.MakeString(u.String())))
		u.ClearQuery()
		if i < 2 {
			u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
			u.AppendKVQuery(api.QueryKeyPart, part)
			last = last.AppendBang(sx.Cons(sx.MakeString("data"), sx.MakeString(u.String())))
			u.ClearQuery()
			u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson)
			u.AppendKVQuery(api.QueryKeyPart, part)
			last.AppendBang(sx.Cons(sx.MakeString("json"), sx.MakeString(u.String())))
			u.ClearQuery()
		}
		i++
	}
	return matrix
}

func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair {
	result := sx.Nil()
	if zl, err := getAllZettel.Run(ctx, zid); err == nil {
		for i := len(zl) - 1; i >= 1; i-- {
			if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
				result = result.Cons(sx.MakeString(boxNo))
			}
		}
	}
	return result
}







|


|
















|




<
<
<
<
|












|





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
		for je := len(encTexts) - 1; je >= 0; je-- {
			enc := encTexts[je]
			if parseOnly {
				u.AppendKVQuery(api.QueryKeyParseOnly, "")
			}
			u.AppendKVQuery(api.QueryKeyPart, part)
			u.AppendKVQuery(api.QueryKeyEncoding, enc)
			row = row.Cons(sx.Cons(sx.String(enc), sx.String(u.String())))
			u.ClearQuery()
		}
		matrix = matrix.Cons(sx.Cons(sx.String(part), row))
	}
	return matrix
}

func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair {
	matrix := wui.infoAPIMatrix(zid, true, encTexts)
	u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))

	for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() {
		line, isLine := sx.GetPair(row.Car())
		if !isLine || line == nil {
			continue
		}
		last := line.LastPair()
		part := apiParts[i]
		u.AppendKVQuery(api.QueryKeyPart, part)
		last = last.AppendBang(sx.Cons(sx.String("plain"), sx.String(u.String())))
		u.ClearQuery()
		if i < 2 {
			u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
			u.AppendKVQuery(api.QueryKeyPart, part)




			last.AppendBang(sx.Cons(sx.String("data"), sx.String(u.String())))
			u.ClearQuery()
		}
		i++
	}
	return matrix
}

func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair {
	result := sx.Nil()
	if zl, err := getAllZettel.Run(ctx, zid); err == nil {
		for i := len(zl) - 1; i >= 1; i-- {
			if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
				result = result.Cons(sx.String(boxNo))
			}
		}
	}
	return result
}

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

13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30

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





82
83
84


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



















109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import (
	"context"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"

	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		q := r.URL.Query()
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder()
		metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		content, endnotes, err := enc.BlocksSxn(&zn.Ast)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		cssRoleURL, err := wui.getCSSRoleURL(ctx, zn.InhMeta)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := server.GetUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user)
		rb.bindSymbol(wui.symMetaHeader, metaObj)
		rb.bindString("css-role-url", sx.MakeString(cssRoleURL))
		rb.bindString("heading", sx.MakeString(title))
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			rb.bindString("role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
		}
		if role, found := zn.InhMeta.Get(api.KeyFolgeRole); found && role != "" {
			rb.bindString("folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
		}
		rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
		rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
		rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
		rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle))
		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		rb.bindString("folge-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyFolge, getTextTitle))
		rb.bindString("subordinate-links", wui.zettelLinksSxn(zn.InhMeta, api.KeySubordinates, getTextTitle))
		rb.bindString("back-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyBack, getTextTitle))
		rb.bindString("successor-links", wui.zettelLinksSxn(zn.InhMeta, api.KeySuccessors, getTextTitle))





		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, env)


		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) getCSSRoleURL(ctx context.Context, m *meta.Meta) (string, error) {
	cssZid, err := wui.retrieveCSSZidFromRole(ctx, m)
	if err != nil {
		return "", err
	}
	if cssZid == id.Invalid {
		return "", nil
	}
	return wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String(), nil
}

func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	if values, ok := m.GetList(key); ok {
		return wui.transformIdentifierSet(values, getTextTitle)
	}
	return nil
}




















func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	values, ok := m.GetList(key)
	if !ok || len(values) == 0 {
		return nil
	}
	return wui.zidLinksSxn(values, getTextTitle)
}

func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) {
	for i := len(values) - 1; i >= 0; i-- {
		val := values[i]
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTextTitle(zid); found > 0 {
			url := sx.MakeString(wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String())
			if title == "" {
				lst = lst.Cons(sx.Cons(sx.MakeString(val), url))
			} else {
				lst = lst.Cons(sx.Cons(sx.MakeString(title), url))
			}
		}
	}
	return lst
}







>











>
|

|










|



<
<
<
<
<
<










<
|

|

|
|







|
|
|
|
>
>
>
>
>



>
>







<
<
<
<
<
<
<
<
<
<
<






>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

















|

|

|





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






50
51
52
53
54
55
56
57
58
59

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93











94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import (
	"context"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		q := r.URL.Query()
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
		metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		content, endnotes, err := enc.BlocksSxn(&zn.Ast)
		if err != nil {






			wui.reportError(ctx, w, err)
			return
		}

		user := server.GetUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user)
		rb.bindSymbol(wui.symMetaHeader, metaObj)

		rb.bindString("heading", sx.String(title))
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			rb.bindString("role-url", sx.String(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
		}
		if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" {
			rb.bindString("folge-role-url", sx.String(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+folgeRole).String()))
		}
		rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
		rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
		rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
		rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle))
		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, api.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "back", zn.InhMeta, api.KeyBack, config.KeyShowBackLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, api.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle)
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			for _, part := range []string{"meta", "actions", "heading"} {
				rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part)
			}
		}
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}












func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	if values, ok := m.GetList(key); ok {
		return wui.transformIdentifierSet(values, getTextTitle)
	}
	return nil
}

func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) {
	varLinks := varPrefix + "-links"
	var symOpen *sx.Symbol
	switch wui.rtConfig.Get(ctx, m, configKey) {
	case "false":
		rb.bindString(varLinks, sx.Nil())
		return
	case "close":
	default:
		symOpen = wui.symAttrOpen
	}
	lstLinks := wui.zettelLinksSxn(m, key, getTextTitle)
	rb.bindString(varLinks, lstLinks)
	if sx.IsNil(lstLinks) {
		return
	}
	rb.bindString(varPrefix+"-open", symOpen)
}

func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	values, ok := m.GetList(key)
	if !ok || len(values) == 0 {
		return nil
	}
	return wui.zidLinksSxn(values, getTextTitle)
}

func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) {
	for i := len(values) - 1; i >= 0; i-- {
		val := values[i]
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTextTitle(zid); found > 0 {
			url := sx.String(wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String())
			if title == "" {
				lst = lst.Cons(sx.Cons(sx.String(val), url))
			} else {
				lst = lst.Cons(sx.Cons(sx.String(title), url))
			}
		}
	}
	return lst
}

Changes to web/adapter/webui/home.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
	"context"
	"errors"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"

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

type getRootStore interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if r.URL.Path != "/" {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}
		homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel))
		apiHomeZid := api.ZettelID(homeZid.String())
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetZettel(ctx, homeZid); err == nil {
				wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))







>













|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
	"context"
	"errors"
	"net/http"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

type getRootStore interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if p := r.URL.Path; p != "/" {
			wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p})
			return
		}
		homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel))
		apiHomeZid := api.ZettelID(homeZid.String())
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetZettel(ctx, homeZid); err == nil {
				wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))

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

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

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

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/client.fossil/sz"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	tx    *szenc.Transformer
	th    *shtml.Transformer

	symAt *sx.Symbol
}

func (wui *WebUI) createGenerator(builder urlBuilder) *htmlGenerator {
	th := shtml.NewTransformer(1, wui.sf)
	symA := wui.symA
	symImg := th.Make("img")
	symAttr := wui.symAttr

	symHref := wui.symHref
	symClass := th.Make("class")
	symTarget := th.Make("target")







<















|
>



|
|







16
17
18
19
20
21
22

23
24
25
26
27
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/client.fossil/api"
	"zettelstore.de/client.fossil/attrs"
	"zettelstore.de/client.fossil/maps"
	"zettelstore.de/client.fossil/shtml"
	"zettelstore.de/client.fossil/sz"
	"zettelstore.de/sx.fossil"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	tx    *szenc.Transformer
	th    *shtml.Evaluator
	lang  string
	symAt *sx.Symbol
}

func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator {
	th := shtml.NewEvaluator(1, wui.sf)
	symA := wui.symA
	symImg := th.Make("img")
	symAttr := wui.symAttr

	symHref := wui.symHref
	symClass := th.Make("class")
	symTarget := th.Make("target")
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
		objA := rest.Car()
		attr, isPair = sx.GetPair(objA)
		if !isPair || !symAttr.IsEqual(attr.Car()) {
			return nil, nil, nil
		}
		return attr, attr.Tail(), rest.Tail()
	}
	linkZettel := func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
		obj, err := prevFn.Call(nil, nil, args)
		if err != nil {
			return sx.Nil()
		}
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}

		hrefP := assoc.Assoc(symHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		zid, fragment, hasFragment := strings.Cut(href.String(), "#")
		u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
		assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String())))
		return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
	}

	th.SetRebinder(func(te *shtml.TransformEnv) {
		te.Rebind(sz.NameSymLinkZettel, linkZettel)
		te.Rebind(sz.NameSymLinkFound, linkZettel)
		te.Rebind(sz.NameSymLinkBased, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
			obj, err := prevFn.Call(nil, nil, args)
			if err != nil {
				return sx.Nil()
			}
			attr, assoc, rest := findA(obj)
			if attr == nil {
				return obj
			}
			hrefP := assoc.Assoc(symHref)
			if hrefP == nil {
				return obj
			}
			href, ok := sx.GetString(hrefP.Cdr())
			if !ok {
				return obj
			}
			u := builder.NewURLBuilder('/').SetRawLocal(href.String())
			assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String())))
			return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
		})
		te.Rebind(sz.NameSymLinkQuery, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
			obj, err := prevFn.Call(nil, nil, args)
			if err != nil {
				return sx.Nil()
			}
			attr, assoc, rest := findA(obj)
			if attr == nil {
				return obj
			}
			hrefP := assoc.Assoc(symHref)
			if hrefP == nil {
				return obj
			}
			href, ok := sx.GetString(hrefP.Cdr())
			if !ok {
				return obj
			}
			ur, err := url.Parse(href.String())
			if err != nil {
				return obj
			}
			q := ur.Query().Get(api.QueryKeyQuery)
			if q == "" {
				return obj
			}
			u := builder.NewURLBuilder('h').AppendQuery(q)
			assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String())))
			return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
		})
		te.Rebind(sz.NameSymLinkExternal, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
			obj, err := prevFn.Call(nil, nil, args)
			if err != nil {
				return sx.Nil()
			}
			attr, assoc, rest := findA(obj)
			if attr == nil {
				return obj
			}
			assoc = assoc.Cons(sx.Cons(symClass, sx.MakeString("external"))).
				Cons(sx.Cons(symTarget, sx.MakeString("_blank"))).
				Cons(sx.Cons(symRel, sx.MakeString("noopener noreferrer")))
			return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
		})
		te.Rebind(sz.NameSymEmbed, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
			obj, err := prevFn.Call(nil, nil, args)
			if err != nil {
				return sx.Nil()
			}
			pair, isPair := sx.GetPair(obj)
			if !isPair || !symImg.IsEqual(pair.Car()) {
				return obj
			}
			attr, isPair := sx.GetPair(pair.Tail().Car())
			if !isPair || !symAttr.IsEqual(attr.Car()) {
				return obj
			}
			symSrc := th.Make("src")
			srcP := attr.Tail().Assoc(symSrc)
			if srcP == nil {
				return obj
			}
			src, isString := sx.GetString(srcP.Cdr())
			if !isString {
				return obj
			}
			zid := api.ZettelID(src)
			if !zid.IsValid() {
				return obj
			}
			u := builder.NewURLBuilder('z').SetZid(zid)
			imgAttr := attr.Tail().Cons(sx.Cons(symSrc, sx.MakeString(u.String()))).Cons(symAttr)
			return pair.Tail().Tail().Cons(imgAttr).Cons(symImg)
		})
	})

	return &htmlGenerator{
		tx:    szenc.NewTransformer(),
		th:    th,

		symAt: symAttr,
	}
}












// SetUnique sets a prefix to make several HTML ids unique.
func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g }

var mapMetaKey = map[string]string{
	api.KeyCopyright: "copyright",
	api.KeyLicense:   "license",
}

func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	tm := g.tx.GetMeta(m, evalMeta)

	hm, err := g.th.Transform(tm)
	if err != nil {
		return nil
	}

	ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
	metaMap := make(map[string]*sx.Pair, m.Length())
	if tags, ok := m.Get(api.KeyTags); ok {







|
<
<
<
<


















|



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





>



>
>
>
>
>
>
>
>
>
>
>











>
|







63
64
65
66
67
68
69
70




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

93
94
95




96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112




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




138
139
140
141
142
143
144
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
		objA := rest.Car()
		attr, isPair = sx.GetPair(objA)
		if !isPair || !symAttr.IsEqual(attr.Car()) {
			return nil, nil, nil
		}
		return attr, attr.Tail(), rest.Tail()
	}
	linkZettel := func(obj sx.Object) sx.Object {




		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}

		hrefP := assoc.Assoc(symHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		zid, fragment, hasFragment := strings.Cut(href.String(), "#")
		u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
		assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String())))
		return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
	}


	rebind(th, sz.NameSymLinkZettel, linkZettel)
	rebind(th, sz.NameSymLinkFound, linkZettel)
	rebind(th, sz.NameSymLinkBased, func(obj sx.Object) sx.Object {




		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		hrefP := assoc.Assoc(symHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		u := builder.NewURLBuilder('/').SetRawLocal(href.String())
		assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String())))
		return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
	})
	rebind(th, sz.NameSymLinkQuery, func(obj sx.Object) sx.Object {




		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		hrefP := assoc.Assoc(symHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		ur, err := url.Parse(href.String())
		if err != nil {
			return obj
		}
		q := ur.Query().Get(api.QueryKeyQuery)
		if q == "" {
			return obj
		}
		u := builder.NewURLBuilder('h').AppendQuery(q)
		assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String())))
		return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
	})
	rebind(th, sz.NameSymLinkExternal, func(obj sx.Object) sx.Object {




		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		assoc = assoc.Cons(sx.Cons(symClass, sx.String("external"))).
			Cons(sx.Cons(symTarget, sx.String("_blank"))).
			Cons(sx.Cons(symRel, sx.String("noopener noreferrer")))
		return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
	})
	rebind(th, sz.NameSymEmbed, func(obj sx.Object) sx.Object {




		pair, isPair := sx.GetPair(obj)
		if !isPair || !symImg.IsEqual(pair.Car()) {
			return obj
		}
		attr, isPair := sx.GetPair(pair.Tail().Car())
		if !isPair || !symAttr.IsEqual(attr.Car()) {
			return obj
		}
		symSrc := th.Make("src")
		srcP := attr.Tail().Assoc(symSrc)
		if srcP == nil {
			return obj
		}
		src, isString := sx.GetString(srcP.Cdr())
		if !isString {
			return obj
		}
		zid := api.ZettelID(src)
		if !zid.IsValid() {
			return obj
		}
		u := builder.NewURLBuilder('z').SetZid(zid)
		imgAttr := attr.Tail().Cons(sx.Cons(symSrc, sx.String(u.String()))).Cons(symAttr)
		return pair.Tail().Tail().Cons(imgAttr).Cons(symImg)

	})

	return &htmlGenerator{
		tx:    szenc.NewTransformer(),
		th:    th,
		lang:  lang,
		symAt: symAttr,
	}
}

func rebind(ev *shtml.Evaluator, name string, fn func(sx.Object) sx.Object) {
	prevFn := ev.ResolveBinding(name)
	ev.Rebind(name, func(args []sx.Object, env *shtml.Environment) sx.Object {
		obj := prevFn(args, env)
		if env.GetError() == nil {
			return fn(obj)
		}
		return sx.Nil()
	})
}

// SetUnique sets a prefix to make several HTML ids unique.
func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g }

var mapMetaKey = map[string]string{
	api.KeyCopyright: "copyright",
	api.KeyLicense:   "license",
}

func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	tm := g.tx.GetMeta(m, evalMeta)
	env := shtml.MakeEnvironment(g.lang)
	hm, err := g.th.Evaluate(tm, &env)
	if err != nil {
		return nil
	}

	ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
	metaMap := make(map[string]*sx.Pair, m.Length())
	if tags, ok := m.Get(api.KeyTags); ok {
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
		}

		newName, found := mapMetaKey[name]
		if !found {
			continue
		}
		a = a.Set("name", newName)
		metaMap[newName] = g.th.TransformMeta(a)
	}
	result := sx.Nil()
	keys := maps.Keys(metaMap)
	for i := len(keys) - 1; i >= 0; i-- {
		result = result.Cons(metaMap[keys[i]])
	}
	return result







|







242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
		}

		newName, found := mapMetaKey[name]
		if !found {
			continue
		}
		a = a.Set("name", newName)
		metaMap[newName] = g.th.EvaluateMeta(a)
	}
	result := sx.Nil()
	keys := maps.Keys(metaMap)
	for i := len(keys) - 1; i >= 0; i-- {
		result = result.Cons(metaMap[keys[i]])
	}
	return result
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
		}
		sb.WriteString(strings.TrimPrefix(val, "#"))
	}
	metaTags := sb.String()
	if len(metaTags) == 0 {
		return nil
	}
	return g.th.TransformMeta(attrs.Attributes{"name": "keywords", "content": metaTags})
}

func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) {
	if bs == nil || len(*bs) == 0 {
		return nil, nil, nil
	}
	sx := g.tx.GetSz(bs)

	sh, err := g.th.Transform(sx)
	if err != nil {
		return nil, nil, err
	}
	return sh, g.th.Endnotes(), nil
}

// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair {
	if is == nil || len(*is) == 0 {
		return nil
	}
	sx := g.tx.GetSz(is)

	sh, err := g.th.Transform(sx)
	if err != nil {
		return nil
	}
	return sh
}







|







>
|



|








>
|





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
		}
		sb.WriteString(strings.TrimPrefix(val, "#"))
	}
	metaTags := sb.String()
	if len(metaTags) == 0 {
		return nil
	}
	return g.th.EvaluateMeta(attrs.Attributes{"name": "keywords", "content": metaTags})
}

func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) {
	if bs == nil || len(*bs) == 0 {
		return nil, nil, nil
	}
	sx := g.tx.GetSz(bs)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil, nil, err
	}
	return sh, g.th.Endnotes(&env), nil
}

// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair {
	if is == nil || len(*is) == 0 {
		return nil
	}
	sx := g.tx.GetSz(is)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil
	}
	return sh
}

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

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
) sx.Object {
	switch kt := meta.Type(key); kt {
	case meta.TypeCredential:
		return sx.MakeString(value)
	case meta.TypeEmpty:
		return sx.MakeString(value)
	case meta.TypeID:
		return wui.transformIdentifier(value, getTextTitle)
	case meta.TypeIDSet:
		return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
	case meta.TypeNumber:
		return wui.transformLink(key, value, value)
	case meta.TypeString:
		return sx.MakeString(value)
	case meta.TypeTagSet:
		return wui.transformTagSet(key, meta.ListFromValue(value))
	case meta.TypeTimestamp:
		if ts, ok := meta.TimeValue(value); ok {
			return sx.MakeList(
				wui.sf.MustMake("time"),
				sx.MakeList(
					wui.symAttr,
					sx.Cons(wui.sf.MustMake("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))),
				),
				sx.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sx.MakeString(ts.Format("2006-01-02&nbsp;15:04:05"))),
			)
		}
		return sx.Nil()
	case meta.TypeURL:
		text := sx.MakeString(value)
		if res, err := wui.url2html([]sx.Object{text}); err == nil {
			return res
		}
		return text
	case meta.TypeWord:
		return wui.transformLink(key, value, value)
	case meta.TypeWordSet:
		return wui.transformWordSet(key, meta.ListFromValue(value))
	case meta.TypeZettelmarkup:
		return wui.transformZmkMetadata(value, evalMetadata, gen)
	default:
		return sx.MakeList(wui.sf.MustMake("b"), sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name))
	}
}

func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
	text := sx.MakeString(val)
	zid, err := id.Parse(val)
	if err != nil {
		return text
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
		attrs := sx.Nil()
		if title != "" {
			attrs = attrs.Cons(sx.Cons(wui.sf.MustMake("title"), sx.MakeString(title)))
		}
		attrs = attrs.Cons(sx.Cons(wui.symHref, sx.MakeString(ub.String()))).Cons(wui.symAttr)
		return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(wui.symA)
	case found == 0:
		return sx.MakeList(wui.sf.MustMake("s"), text)
	default: // case found < 0:
		return text
	}
}

func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
	if len(vals) == 0 {
		return nil
	}
	space := sx.MakeString(" ")
	text := make([]sx.Object, 0, 2*len(vals))
	for _, val := range vals {
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
	}
	return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}

func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
	if len(tags) == 0 {
		return nil
	}
	space := sx.MakeString(" ")
	text := make([]sx.Object, 0, 2*len(tags))
	for _, tag := range tags {
		text = append(text, space, wui.transformLink(key, tag, tag))
	}
	return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}

func (wui *WebUI) transformWordSet(key string, words []string) sx.Object {
	if len(words) == 0 {
		return sx.Nil()
	}
	space := sx.MakeString(" ")
	text := make([]sx.Object, 0, 2*len(words))
	for _, word := range words {
		text = append(text, space, wui.transformLink(key, word, word))
	}
	return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}

func (wui *WebUI) transformLink(key, value, text string) *sx.Pair {
	return sx.MakeList(
		wui.symA,
		sx.MakeList(
			wui.symAttr,
			sx.Cons(wui.symHref, sx.MakeString(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())),
		),
		sx.MakeString(text),
	)
}

type evalMetadataFunc = func(string) ast.InlineSlice

func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
	return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }







|

|







|








|

|




<
<
|
<
<







|




|










|

|
|











|











|











|












|

|







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


62


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
) sx.Object {
	switch kt := meta.Type(key); kt {
	case meta.TypeCredential:
		return sx.String(value)
	case meta.TypeEmpty:
		return sx.String(value)
	case meta.TypeID:
		return wui.transformIdentifier(value, getTextTitle)
	case meta.TypeIDSet:
		return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
	case meta.TypeNumber:
		return wui.transformLink(key, value, value)
	case meta.TypeString:
		return sx.String(value)
	case meta.TypeTagSet:
		return wui.transformTagSet(key, meta.ListFromValue(value))
	case meta.TypeTimestamp:
		if ts, ok := meta.TimeValue(value); ok {
			return sx.MakeList(
				wui.sf.MustMake("time"),
				sx.MakeList(
					wui.symAttr,
					sx.Cons(wui.sf.MustMake("datetime"), sx.String(ts.Format("2006-01-02T15:04:05"))),
				),
				sx.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sx.String(ts.Format("2006-01-02&nbsp;15:04:05"))),
			)
		}
		return sx.Nil()
	case meta.TypeURL:


		return wui.url2html(sx.String(value))


	case meta.TypeWord:
		return wui.transformLink(key, value, value)
	case meta.TypeWordSet:
		return wui.transformWordSet(key, meta.ListFromValue(value))
	case meta.TypeZettelmarkup:
		return wui.transformZmkMetadata(value, evalMetadata, gen)
	default:
		return sx.MakeList(wui.sf.MustMake("b"), sx.String("Unhandled type: "), sx.String(kt.Name))
	}
}

func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
	text := sx.String(val)
	zid, err := id.Parse(val)
	if err != nil {
		return text
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
		attrs := sx.Nil()
		if title != "" {
			attrs = attrs.Cons(sx.Cons(wui.sf.MustMake("title"), sx.String(title)))
		}
		attrs = attrs.Cons(sx.Cons(wui.symHref, sx.String(ub.String()))).Cons(wui.symAttr)
		return sx.Nil().Cons(sx.String(zid.String())).Cons(attrs).Cons(wui.symA)
	case found == 0:
		return sx.MakeList(wui.sf.MustMake("s"), text)
	default: // case found < 0:
		return text
	}
}

func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
	if len(vals) == 0 {
		return nil
	}
	space := sx.String(" ")
	text := make([]sx.Object, 0, 2*len(vals))
	for _, val := range vals {
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
	}
	return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}

func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
	if len(tags) == 0 {
		return nil
	}
	space := sx.String(" ")
	text := make([]sx.Object, 0, 2*len(tags))
	for _, tag := range tags {
		text = append(text, space, wui.transformLink(key, tag, tag))
	}
	return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}

func (wui *WebUI) transformWordSet(key string, words []string) sx.Object {
	if len(words) == 0 {
		return sx.Nil()
	}
	space := sx.String(" ")
	text := make([]sx.Object, 0, 2*len(words))
	for _, word := range words {
		text = append(text, space, wui.transformLink(key, word, word))
	}
	return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}

func (wui *WebUI) transformLink(key, value, text string) *sx.Pair {
	return sx.MakeList(
		wui.symA,
		sx.MakeList(
			wui.symAttr,
			sx.Cons(wui.symHref, sx.String(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())),
		),
		sx.String(text),
	)
}

type evalMetadataFunc = func(string) ast.InlineSlice

func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
	return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }

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

10
11
12
13
14
15
16


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







38
39
40
41
42
43
44
45
46















47
48
49
50
51
52
53
54
55

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


















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


96
97
98
99
100
101












































102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139































package webui

import (
	"context"
	"io"
	"net/http"


	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {







		q := adapter.GetQuery(r.URL.Query())
		q = q.SetDeterministic()
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(ctx, q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if actions := q.Actions(); len(actions) > 0 {















			switch actions[0] {
			case "ATOM":
				wui.renderAtom(w, q, metaSeq)
				return
			case "RSS":
				wui.renderRSS(ctx, w, q, metaSeq)
				return
			}
		}

		var content, endnotes *sx.Pair
		if bn := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil {
			enc := wui.getSimpleHTMLEncoder()
			content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
		}

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "list",
			wui.rtConfig.Get(ctx, nil, api.KeyLang),
			wui.rtConfig.GetSiteName(), user)
		if q == nil {
			rb.bindString("heading", sx.MakeString(wui.rtConfig.GetSiteName()))
		} else {
			var sb strings.Builder
			q.PrintHuman(&sb)
			rb.bindString("heading", sx.MakeString(sb.String()))
		}
		rb.bindString("query-value", sx.MakeString(q.String()))


















		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		apiURL := wui.NewURLBuilder('z').AppendQuery(q.String())
		seed, found := q.GetSeed()
		if found {
			apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed))
		} else {
			seed = 0
		}
		rb.bindString("plain-url", sx.MakeString(apiURL.String()))
		rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String()))
		if wui.canCreate(ctx, user) {
			rb.bindString("create-url", sx.MakeString(wui.createNewURL))
			rb.bindString("seed", sx.Int64(seed))
		}
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env)


		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}













































func (wui *WebUI) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var rssConfig rss.Configuration
	rssConfig.Setup(ctx, wui.rtConfig)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" {
		rssConfig.Title = strings.Join(actions[2:], " ")
	}
	data := rssConfig.Marshal(q, ml)

	adapter.PrepareHeader(w, rss.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
	}
	if err != nil {
		wui.log.IfErr(err).Msg("unable to write RSS data")
	}
}

func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var atomConfig atom.Configuration
	atomConfig.Setup(wui.rtConfig)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" {
		atomConfig.Title = strings.Join(actions[2:], " ")
	}
	data := atomConfig.Marshal(q, ml)

	adapter.PrepareHeader(w, atom.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
	}
	if err != nil {
		wui.log.IfErr(err).Msg("unable to write Atom data")
	}
}





































>
>



















|

>
>
>
>
>
>
>
|








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


|













|



|

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









|
|

|




>
>






>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
















|


















|


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

package webui

import (
	"context"
	"io"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		urlQuery := r.URL.Query()
		if wui.handleTagZettel(w, r, tagZettel, urlQuery) {
			return
		}
		if wui.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}
		q := adapter.GetQuery(urlQuery)
		q = q.SetDeterministic()
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(ctx, q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if actions := q.Actions(); len(actions) > 0 {
			var tempActions []string
			for _, act := range actions {
				if act == "REINDEX" {
					for _, m := range metaSeq {
						if err = reIndex.Run(ctx, m.Zid); err != nil {
							wui.reportError(ctx, w, err)
							return
						}
					}
					continue
				}
				tempActions = append(tempActions, act)
			}
			actions = tempActions
			if len(actions) > 0 {
				switch actions[0] {
				case "ATOM":
					wui.renderAtom(w, q, metaSeq)
					return
				case "RSS":
					wui.renderRSS(ctx, w, q, metaSeq)
					return
				}
			}
		}
		var content, endnotes *sx.Pair
		if bn := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil {
			enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, nil, api.KeyLang))
			content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
		}

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "list",
			wui.rtConfig.Get(ctx, nil, api.KeyLang),
			wui.rtConfig.GetSiteName(), user)
		if q == nil {
			rb.bindString("heading", sx.String(wui.rtConfig.GetSiteName()))
		} else {
			var sb strings.Builder
			q.PrintHuman(&sb)
			rb.bindString("heading", sx.String(sb.String()))
		}
		rb.bindString("query-value", sx.String(q.String()))
		if tzl := q.GetMetaValues(api.KeyTags); len(tzl) > 0 {
			sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl)
			if !sx.IsNil(sxTzl) {
				rb.bindString("tag-zettel", sxTzl)
			}
			if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) {
				rb.bindString("create-tag-zettel", sxNoTzl)
			}
		}
		if rzl := q.GetMetaValues(api.KeyRole); len(rzl) > 0 {
			sxRzl, sxNoRzl := wui.transformRoleZettelList(ctx, roleZettel, rzl)
			if !sx.IsNil(sxRzl) {
				rb.bindString("role-zettel", sxRzl)
			}
			if !sx.IsNil(sxNoRzl) && wui.canCreate(ctx, user) {
				rb.bindString("create-role-zettel", sxNoRzl)
			}
		}
		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		apiURL := wui.NewURLBuilder('z').AppendQuery(q.String())
		seed, found := q.GetSeed()
		if found {
			apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed))
		} else {
			seed = 0
		}
		rb.bindString("plain-url", sx.String(apiURL.String()))
		rb.bindString("data-url", sx.String(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String()))
		if wui.canCreate(ctx, user) {
			rb.bindString("create-url", sx.String(wui.createNewURL))
			rb.bindString("seed", sx.Int64(seed))
		}
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	}
}

func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) {
	slices.Reverse(tags)
	for _, tag := range tags {
		tag = meta.NormalizeTag(tag)
		if _, err := tagZettel.Run(ctx, tag); err == nil {
			u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyTag, tag)
			withZettel = wui.prependZettelLink(withZettel, tag, u)
		} else {
			u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewTag).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, tag)
			withoutZettel = wui.prependZettelLink(withoutZettel, tag, u)
		}
	}
	return withZettel, withoutZettel
}

func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []string) (withZettel, withoutZettel *sx.Pair) {
	slices.Reverse(roles)
	for _, role := range roles {
		if _, err := roleZettel.Run(ctx, role); err == nil {
			u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyRole, role)
			withZettel = wui.prependZettelLink(withZettel, role, u)
		} else {
			u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewRole).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, role)
			withoutZettel = wui.prependZettelLink(withoutZettel, role, u)
		}
	}
	return withZettel, withoutZettel
}

func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair {
	link := sx.MakeList(
		wui.symA,
		sx.MakeList(
			wui.symAttr,
			sx.Cons(wui.symHref, sx.String(u.String())),
		),
		sx.String(name),
	)
	if sxZtl != nil {
		sxZtl = sxZtl.Cons(sx.String(", "))
	}
	return sxZtl.Cons(link)
}

func (wui *WebUI) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var rssConfig rss.Configuration
	rssConfig.Setup(ctx, wui.rtConfig)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" {
		rssConfig.Title = strings.Join(actions[2:], " ")
	}
	data := rssConfig.Marshal(q, ml)

	adapter.PrepareHeader(w, rss.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
	}
	if err != nil {
		wui.log.Error().Err(err).Msg("unable to write RSS data")
	}
}

func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var atomConfig atom.Configuration
	atomConfig.Setup(wui.rtConfig)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" {
		atomConfig.Title = strings.Join(actions[2:], " ")
	}
	data := atomConfig.Marshal(q, ml)

	adapter.PrepareHeader(w, atom.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
	}
	if err != nil {
		wui.log.Error().Err(err).Msg("unable to write Atom data")
	}
}

func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)
	if tag == "" {
		return false
	}
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, tag)
	if err != nil {
		wui.reportError(ctx, w, err)
		return true
	}
	wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(z.Meta.Zid.String())))
	return true
}

func (wui *WebUI) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool {
	role := vals.Get(api.QueryKeyRole)
	if role == "" {
		return false
	}
	ctx := r.Context()
	z, err := roleZettel.Run(ctx, role)
	if err != nil {
		wui.reportError(ctx, w, err)
		return true
	}
	wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(z.Meta.Zid.String())))
	return true
}

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

24
25
26
27
28
29
30

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


52
53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71
72
)

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

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			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)


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

		curZid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			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







>
|

|


















>
>











>
|

|







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

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

Added web/adapter/webui/sxn_code.go.























































































































































































































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

package webui

import (
	"context"
	"fmt"
	"io"

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

func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (id.Digraph, sxeval.Environment, error) {
	// getMeta MUST currently use GetZettel, because GetMeta just uses the
	// Index, which might not be current.
	getMeta := func(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
		z, err := wui.box.GetZettel(ctx, zid)
		if err != nil {
			return nil, err
		}
		return z.Meta, nil
	}
	dg := buildSxnCodeDigraph(ctx, id.StartSxnZid, getMeta)
	if dg == nil {
		return nil, wui.engine.RootEnvironment(), nil
	}
	dg = dg.AddVertex(id.BaseSxnZid).AddEdge(id.StartSxnZid, id.BaseSxnZid)
	dg = dg.AddVertex(id.PreludeSxnZid).AddEdge(id.BaseSxnZid, id.PreludeSxnZid)
	dg = dg.TransitiveClosure(id.StartSxnZid)

	if zid, isDAG := dg.IsDAG(); !isDAG {
		return nil, nil, fmt.Errorf("zettel %v is part of a dependency cycle", zid)
	}
	env := sxeval.MakeChildEnvironment(wui.engine.RootEnvironment(), "zettel", 128)
	for _, zid := range dg.SortReverse() {
		if err := wui.loadSxnCodeZettel(ctx, zid, env); err != nil {
			return nil, nil, err
		}
	}
	return dg, env, nil
}

type getMetaFunc func(context.Context, id.Zid) (*meta.Meta, error)

func buildSxnCodeDigraph(ctx context.Context, startZid id.Zid, getMeta getMetaFunc) id.Digraph {
	m, err := getMeta(ctx, startZid)
	if err != nil {
		return nil
	}
	var marked id.Set
	stack := []*meta.Meta{m}
	dg := id.Digraph(nil).AddVertex(startZid)
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr.Zid) {
			continue
		}
		marked = marked.Add(curr.Zid)
		if precursors, hasPrecursor := curr.GetList(api.KeyPrecursor); hasPrecursor && len(precursors) > 0 {
			for _, pre := range precursors {
				if preZid, errParse := id.Parse(pre); errParse == nil {
					m, err = getMeta(ctx, preZid)
					if err != nil {
						continue
					}
					stack = append(stack, m)
					dg.AddVertex(preZid)
					dg.AddEdge(curr.Zid, preZid)
				}
			}
		}
	}
	return dg
}

func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, env sxeval.Environment) error {
	rdr, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return err
	}
	for {
		form, err2 := rdr.Read()
		if err2 != nil {
			if err2 == io.EOF {
				return nil
			}
			return err2
		}
		wui.log.Debug().Zid(zid).Str("form", form.Repr()).Msg("Loaded sxn code")

		if _, err2 = wui.engine.Eval(form, env, nil); err2 != nil {
			return err2
		}
	}
}

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

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

52
53
54
55
56
57

58





59






60





61











62





63


64
65





66
67
68
69





70
71
72
73


74






















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
















89
90
91
92
93

94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160













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

package webui

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxbuiltins"
	"zettelstore.de/sx.fossil/sxbuiltins/binding"
	"zettelstore.de/sx.fossil/sxbuiltins/boolean"
	"zettelstore.de/sx.fossil/sxbuiltins/callable"
	"zettelstore.de/sx.fossil/sxbuiltins/cond"
	"zettelstore.de/sx.fossil/sxbuiltins/define"
	"zettelstore.de/sx.fossil/sxbuiltins/env"
	"zettelstore.de/sx.fossil/sxbuiltins/list"
	"zettelstore.de/sx.fossil/sxbuiltins/quote"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) createRenderEngine() *sxeval.Engine {
	root := sx.MakeRootEnvironment()
	engine := sxeval.MakeEngine(wui.sf, root)
	quote.InstallQuoteSyntax(root, wui.symQuote)
	quote.InstallQuasiQuoteSyntax(root, wui.symQQ, wui.symUQ, wui.symUQS)
	engine.BindSyntax("if", cond.IfS)

	engine.BindSyntax("and", boolean.AndS)
	engine.BindSyntax("or", boolean.OrS)
	engine.BindSyntax("lambda", callable.LambdaS)
	engine.BindSyntax("define", define.DefineS)
	engine.BindSyntax("let", binding.LetS)
	engine.BindBuiltinEEA("bound?", env.BoundP)

	engine.BindBuiltinEEA("map", callable.Map)





	engine.BindBuiltinEEA("apply", callable.Apply)






	engine.BindBuiltinA("list", list.List)





	engine.BindBuiltinA("append", list.Append)











	engine.BindBuiltinA("car", list.Car)





	engine.BindBuiltinA("cdr", list.Cdr)



	engine.BindBuiltinA("url-to-html", wui.url2html)





	return engine
}

func (wui *WebUI) url2html(args []sx.Object) (sx.Object, error) {





	err := sxbuiltins.CheckArgs(args, 1, 1)
	text, err := sxbuiltins.GetString(err, args, 0)
	if err != nil {
		return nil, err


	}






















	if u, errURL := url.Parse(text.String()); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				wui.symA,
				sx.MakeList(
					wui.symAttr,
					sx.Cons(wui.symHref, sx.MakeString(us)),
					sx.Cons(wui.sf.MustMake("target"), sx.MakeString("_blank")),
					sx.Cons(wui.sf.MustMake("rel"), sx.MakeString("noopener noreferrer")),
				),
				text), nil
		}
	}
	return text, nil
















}

// createRenderEnv creates a new environment and populates it with all relevant data for the base template.
func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (sx.Environment, renderBinder) {
	userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)

	env := sx.MakeChildEnvironment(wui.engine.RootEnvironment(), name, 128)
	rb := makeRenderBinder(wui.sf, env, nil)
	rb.bindString("lang", sx.MakeString(lang))
	rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL))
	rb.bindString("css-role-url", sx.MakeString(""))
	rb.bindString("title", sx.MakeString(title))
	rb.bindString("home-url", sx.MakeString(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
	rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid))
	rb.bindString("user-zettel-url", sx.MakeString(userZettelURL))
	rb.bindString("user-ident", sx.MakeString(userIdent))
	rb.bindString("login-url", sx.MakeString(wui.loginURL))
	rb.bindString("logout-url", sx.MakeString(wui.logoutURL))
	rb.bindString("list-zettel-url", sx.MakeString(wui.listZettelURL))
	rb.bindString("list-roles-url", sx.MakeString(wui.listRolesURL))
	rb.bindString("list-tags-url", sx.MakeString(wui.listTagsURL))
	if wui.canRefresh(user) {
		rb.bindString("refresh-url", sx.MakeString(wui.refreshURL))
	}
	rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user))
	rb.bindString("search-url", sx.MakeString(wui.searchURL))
	rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed))
	rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
	rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
	rb.bindSymbol(wui.symMetaHeader, sx.Nil())
	rb.bindSymbol(wui.symDetail, sx.Nil())
	return env, rb
}

func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) {
	if user == nil {
		return false, "", ""
	}
	return true, wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String(), user.GetDefault(api.KeyUserID, "")
}

type renderBinder struct {
	err  error
	make func(string) (*sx.Symbol, error)
	bind func(*sx.Symbol, sx.Object) error
}

func makeRenderBinder(sf sx.SymbolFactory, env sx.Environment, err error) renderBinder {
	return renderBinder{make: sf.Make, bind: env.Bind, err: err}
}
func (rb *renderBinder) bindString(key string, obj sx.Object) {
	if rb.err == nil {
		sym, err := rb.make(key)
		if err == nil {
			rb.err = rb.bind(sym, obj)
			return
		}
		rb.err = err
	}
}
func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) {
	if rb.err == nil {
		rb.err = rb.bind(sym, obj)
	}
}
func (rb *renderBinder) bindKeyValue(key string, value string) {
	rb.bindString("meta-"+key, sx.MakeString(value))
	if kt := meta.Type(key); kt.IsSet {
		rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value)))
	}













}

func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
	strZid := m.Zid.String()
	apiZid := api.ZettelID(strZid)
	newURLBuilder := wui.NewURLBuilder

	rb.bindString("zid", sx.MakeString(strZid))
	rb.bindString("web-url", sx.MakeString(wui.NewURLBuilder('h').SetZid(apiZid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String()))
	}
	rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}
		rb.bindString("version-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		rb.bindString("child-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String()))
		rb.bindString("folge-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canRename(ctx, user, m) {
		rb.bindString("rename-url", sx.MakeString(wui.NewURLBuilder('b').SetZid(apiZid).String()))
	}
	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.MakeString(wui.NewURLBuilder('d').SetZid(apiZid).String()))
	}
	if val, found := m.Get(api.KeyUselessFiles); found {
		rb.bindString("useless", sx.Cons(sx.MakeString(val), nil))
	}
	rb.bindString("context-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(strZid+" "+api.ContextDirective).String()))




	// Ensure to have title, role, tags, and syntax included as "meta-*"
	rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, ""))
	rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, ""))
	rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, ""))
	rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, ""))
	sentinel := sx.Cons(nil, nil)
	curr := sentinel
	for _, p := range m.ComputedPairs() {
		key, value := p.Key, p.Value
		curr = curr.AppendBang(sx.Cons(sx.MakeString(key), sx.MakeString(value)))

		rb.bindKeyValue(key, value)
	}
	rb.bindString("metapairs", sentinel.Tail())
}

func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) {







<






<
<
<
<
<
<
<
<















|

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



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

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






|
|
|

|


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



|

>
|
|
|
|
|
<
|
|


|
|
|
|
|
|
|

|


|
|
|

















|


|
|





|







|



|



>
>
>
>
>
>
>
>
>
>
>
>
>







|
|

|

|


|

|
|
|


|


|


|

|
>
>
>










|







10
11
12
13
14
15
16

17
18
19
20
21
22








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

41
42
43




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

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

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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

package webui

import (
	"bytes"
	"context"
	"fmt"

	"net/http"
	"net/url"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxbuiltins"








	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"
	"zettelstore.de/sx.fossil/sxreader"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) createRenderEngine() *sxeval.Engine {
	root := sxeval.MakeRootEnvironment(len(specials) + len(builtins) + 3)
	engine := sxeval.MakeEngine(wui.sf, root)
	for _, syntax := range specials {

		engine.BindSpecial(syntax)
	}
	for _, b := range builtins {




		engine.BindBuiltin(b)
	}
	engine.BindBuiltin(&sxeval.Builtin{
		Name:     "url-to-html",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) {
			text, err := sxbuiltins.GetString(args, 0)
			if err != nil {
				return nil, err
			}
			return wui.url2html(text), nil
		},
	})
	engine.BindBuiltin(&sxeval.Builtin{
		Name:     "zid-content-path",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) {
			s, err := sxbuiltins.GetString(args, 0)
			if err != nil {
				return nil, err
			}
			zid, err := id.Parse(s.String())
			if err != nil {
				return nil, fmt.Errorf("parsing zettel identfier %q: %w", s, err)
			}
			ub := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))
			return sx.String(ub.String()), nil
		},
	})
	engine.BindBuiltin(&sxeval.Builtin{
		Name:     "query->url",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) {
			qs, err := sxbuiltins.GetString(args, 0)
			if err != nil {
				return nil, err
			}

			u := wui.NewURLBuilder('h').AppendQuery(qs.String())
			return sx.String(u.String()), nil
		},
	})
	root.Freeze()
	return engine
}

var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefVarS, &sxbuiltins.DefConstS, // defvar, defconst
		&sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda
		&sxbuiltins.SetXS,     // set!
		&sxbuiltins.CondS,     // cond
		&sxbuiltins.IfS,       // if

		&sxbuiltins.BeginS,    // begin
		&sxbuiltins.DefMacroS, // defmacro
	}
	builtins = []*sxeval.Builtin{
		&sxbuiltins.Identical,            // ==
		&sxbuiltins.NullP,                // null?
		&sxbuiltins.PairP,                // pair?
		&sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr
		&sxbuiltins.Caar, &sxbuiltins.Cadr, &sxbuiltins.Cdar, &sxbuiltins.Cddr,
		&sxbuiltins.Caaar, &sxbuiltins.Caadr, &sxbuiltins.Cadar, &sxbuiltins.Caddr,
		&sxbuiltins.Cdaar, &sxbuiltins.Cdadr, &sxbuiltins.Cddar, &sxbuiltins.Cdddr,
		&sxbuiltins.List,         // list
		&sxbuiltins.Append,       // append
		&sxbuiltins.Assoc,        // assoc
		&sxbuiltins.Map,          // map
		&sxbuiltins.Apply,        // apply
		&sxbuiltins.StringAppend, // string-append
		&sxbuiltins.BoundP,       // bound?
		&sxbuiltins.Defined,      // defined?
		&sxbuiltins.CurrentEnv,   // current-environment
		&sxbuiltins.EnvLookup,    // environment-lookup
	}
)

func (wui *WebUI) url2html(text sx.String) sx.Object {
	if u, errURL := url.Parse(text.String()); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				wui.symA,
				sx.MakeList(
					wui.symAttr,
					sx.Cons(wui.symHref, sx.String(us)),
					sx.Cons(wui.sf.MustMake("target"), sx.String("_blank")),
					sx.Cons(wui.sf.MustMake("rel"), sx.String("noopener noreferrer")),
				),
				text)
		}
	}
	return text
}

func (wui *WebUI) getParentEnv(ctx context.Context) (sxeval.Environment, error) {
	wui.mxZettelEnv.Lock()
	defer wui.mxZettelEnv.Unlock()
	if parentEnv := wui.zettelEnv; parentEnv != nil {
		return parentEnv, nil
	}
	dag, zettelEnv, err := wui.loadAllSxnCodeZettel(ctx)
	if err != nil {
		wui.log.Error().Err(err).Msg("loading zettel sxn")
		return nil, err
	}
	wui.dag = dag
	wui.zettelEnv = zettelEnv
	return zettelEnv, nil
}

// createRenderEnv creates a new environment and populates it with all relevant data for the base template.
func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (sxeval.Environment, renderBinder) {
	userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)
	parentEnv, err := wui.getParentEnv(ctx)
	env := sxeval.MakeChildEnvironment(parentEnv, name, 128)
	rb := makeRenderBinder(wui.sf, env, err)
	rb.bindString("lang", sx.String(lang))
	rb.bindString("css-base-url", sx.String(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.String(wui.cssUserURL))

	rb.bindString("title", sx.String(title))
	rb.bindString("home-url", sx.String(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
	rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid))
	rb.bindString("user-zettel-url", sx.String(userZettelURL))
	rb.bindString("user-ident", sx.String(userIdent))
	rb.bindString("login-url", sx.String(wui.loginURL))
	rb.bindString("logout-url", sx.String(wui.logoutURL))
	rb.bindString("list-zettel-url", sx.String(wui.listZettelURL))
	rb.bindString("list-roles-url", sx.String(wui.listRolesURL))
	rb.bindString("list-tags-url", sx.String(wui.listTagsURL))
	if wui.canRefresh(user) {
		rb.bindString("refresh-url", sx.String(wui.refreshURL))
	}
	rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user))
	rb.bindString("search-url", sx.String(wui.searchURL))
	rb.bindString("query-key-query", sx.String(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.String(api.QueryKeySeed))
	rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
	rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
	rb.bindSymbol(wui.symMetaHeader, sx.Nil())
	rb.bindSymbol(wui.symDetail, sx.Nil())
	return env, rb
}

func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) {
	if user == nil {
		return false, "", ""
	}
	return true, wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String(), user.GetDefault(api.KeyUserID, "")
}

type renderBinder struct {
	err  error
	make func(string) (*sx.Symbol, error)
	env  sxeval.Environment
}

func makeRenderBinder(sf sx.SymbolFactory, env sxeval.Environment, err error) renderBinder {
	return renderBinder{make: sf.Make, env: env, err: err}
}
func (rb *renderBinder) bindString(key string, obj sx.Object) {
	if rb.err == nil {
		sym, err := rb.make(key)
		if err == nil {
			rb.err = rb.env.Bind(sym, obj)
			return
		}
		rb.err = err
	}
}
func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) {
	if rb.err == nil {
		rb.err = rb.env.Bind(sym, obj)
	}
}
func (rb *renderBinder) bindKeyValue(key string, value string) {
	rb.bindString("meta-"+key, sx.String(value))
	if kt := meta.Type(key); kt.IsSet {
		rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value)))
	}
}
func (rb *renderBinder) rebindResolved(key, defKey string) {
	if rb.err == nil {
		sym, err := rb.make(key)
		if err == nil {
			if obj, found := sxeval.Resolve(rb.env, sym); found {
				rb.bindString(defKey, obj)
				return
			}
			return
		}
		rb.err = err
	}
}

func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
	strZid := m.Zid.String()
	apiZid := api.ZettelID(strZid)
	newURLBuilder := wui.NewURLBuilder

	rb.bindString("zid", sx.String(strZid))
	rb.bindString("web-url", sx.String(newURLBuilder('h').SetZid(apiZid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.String(newURLBuilder('e').SetZid(apiZid).String()))
	}
	rb.bindString("info-url", sx.String(newURLBuilder('i').SetZid(apiZid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}
		rb.bindString("version-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		rb.bindString("child-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String()))
		rb.bindString("folge-url", sx.String(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canRename(ctx, user, m) {
		rb.bindString("rename-url", sx.String(newURLBuilder('b').SetZid(apiZid).String()))
	}
	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.String(newURLBuilder('d').SetZid(apiZid).String()))
	}
	if val, found := m.Get(api.KeyUselessFiles); found {
		rb.bindString("useless", sx.Cons(sx.String(val), nil))
	}
	rb.bindString("context-url", sx.String(newURLBuilder('h').AppendQuery(strZid+" "+api.ContextDirective).String()))
	if wui.canRefresh(user) {
		rb.bindString("reindex-url", sx.String(newURLBuilder('h').AppendQuery(strZid+" "+api.IdentDirective+api.ActionSeparator+"REINDEX").String()))
	}

	// Ensure to have title, role, tags, and syntax included as "meta-*"
	rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, ""))
	rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, ""))
	rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, ""))
	rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, ""))
	sentinel := sx.Cons(nil, nil)
	curr := sentinel
	for _, p := range m.ComputedPairs() {
		key, value := p.Key, p.Value
		curr = curr.AppendBang(sx.Cons(sx.String(key), sx.String(value)))

		rb.bindKeyValue(key, value)
	}
	rb.bindString("metapairs", sentinel.Tail())
}

func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) {
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
		z, err2 := wui.box.GetZettel(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, z.Meta) {
			continue
		}
		text := sx.MakeString(parser.NormalizedSpacedText(z.Meta.GetTitle()))
		link := sx.MakeString(wui.NewURLBuilder('c').SetZid(api.ZettelID(zid.String())).
			AppendKVQuery(queryKeyAction, valueActionNew).String())

		lst = lst.Cons(sx.Cons(text, link))
	}
	return lst
}
func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
	if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := wui.getSimpleHTMLEncoder().SetUnique("footer-")
			if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil {
				if content != nil && endnotes != nil {
					content.LastPair().SetCdr(sx.Cons(endnotes, nil))
				}
				return content
			}
		}
	}
	return nil
}

func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid) error {
	if expr := wui.getSxnCache(zid); expr != nil {
		return nil
	}
	rdr, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return err
	}
	for {
		form, err2 := rdr.Read()
		if err2 != nil {
			if err2 == io.EOF {
				wui.setSxnCache(zid, sxeval.TrueExpr) // Hack to load only once
				return nil
			}
			return err2
		}
		wui.log.Trace().Str("form", form.Repr()).Msg("Load sxn code")

		_, err2 = wui.engine.Eval(wui.engine.GetToplevelEnv(), form)
		if err2 != nil {
			return err2
		}
	}
}

func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env sx.Environment) (sxeval.Expr, error) {
	if t := wui.getSxnCache(zid); t != nil {
		return t, nil
	}

	reader, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return nil, err
	}

	objs, err := reader.ReadAll()
	if err != nil {
		wui.log.IfErr(err).Zid(zid).Msg("reading sxn template")
		return nil, err
	}
	if len(objs) != 1 {
		return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs))
	}
	t, err := wui.engine.Parse(env, objs[0])
	if err != nil {
		return nil, err
	}

	wui.setSxnCache(zid, wui.engine.Rework(env, t))
	return t, nil
}
func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) {
	ztl, err := wui.box.GetZettel(ctx, zid)
	if err != nil {
		return nil, err
	}

	reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()), sxreader.WithSymbolFactory(wui.sf))
	quote.InstallQuoteReader(reader, wui.symQuote, '\'')
	quote.InstallQuasiQuoteReader(reader, wui.symQQ, '`', wui.symUQ, ',', wui.symUQS, '@')
	return reader, nil
}

func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env sx.Environment) (sx.Object, error) {
	templateExpr, err := wui.getSxnTemplate(ctx, zid, env)
	if err != nil {
		return nil, err
	}
	return wui.engine.Execute(env, templateExpr)
}

func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env sx.Environment) error {
	return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, env)
}
func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, env sx.Environment) error {
	err := wui.loadSxnCodeZettel(ctx, id.TemplateSxnZid)
	if err != nil {
		return err
	}
	detailObj, err := wui.evalSxnTemplate(ctx, templateID, env)
	if err != nil {
		return err
	}
	env.Bind(wui.symDetail, detailObj)

	pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, env)
	if err != nil {
		return err
	}
	wui.log.Debug().Str("page", pageObj.Repr()).Msg("render")

	gen := sxhtml.NewGenerator(wui.sf, sxhtml.WithNewline)
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	_, err = w.Write(sb.Bytes())
	wui.log.IfErr(err).Msg("Unable to write HTML via template")

	return nil // No error reporting, since we do not know what happended during write to client.
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	} else {
		wui.log.Trace().Err(err).Msg("reportError")
	}
	user := server.GetUser(ctx)
	env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user)
	rb.bindString("heading", sx.MakeString(http.StatusText(code)))
	rb.bindString("message", sx.MakeString(text))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.ErrorTemplateZid, env)
	}
	if errBind := rb.err; errBind != nil {



		wui.log.Error().Err(errBind).Msg("while rendering error message")
		fmt.Fprintf(w, "Error while rendering error message: %v", errBind)
	}












}

func makeStringList(sl []string) *sx.Pair {
	if len(sl) == 0 {
		return nil
	}
	result := sx.Nil()
	for i := len(sl) - 1; i >= 0; i-- {
		result = result.Cons(sx.MakeString(sl[i]))
	}
	return result
}







|
|









|











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











|





|




|









<
<



|




|


|


|
<
<
<
<



















|
|
>








|



|
|

|

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








|



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339


























340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387




388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431

432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
		z, err2 := wui.box.GetZettel(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, z.Meta) {
			continue
		}
		text := sx.String(parser.NormalizedSpacedText(z.Meta.GetTitle()))
		link := sx.String(wui.NewURLBuilder('c').SetZid(api.ZettelID(zid.String())).
			AppendKVQuery(queryKeyAction, valueActionNew).String())

		lst = lst.Cons(sx.Cons(text, link))
	}
	return lst
}
func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
	if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)).SetUnique("footer-")
			if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil {
				if content != nil && endnotes != nil {
					content.LastPair().SetCdr(sx.Cons(endnotes, nil))
				}
				return content
			}
		}
	}
	return nil
}



























func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env sxeval.Environment) (sxeval.Expr, error) {
	if t := wui.getSxnCache(zid); t != nil {
		return t, nil
	}

	reader, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return nil, err
	}

	objs, err := reader.ReadAll()
	if err != nil {
		wui.log.Error().Err(err).Zid(zid).Msg("reading sxn template")
		return nil, err
	}
	if len(objs) != 1 {
		return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs))
	}
	t, err := wui.engine.Parse(objs[0], env)
	if err != nil {
		return nil, err
	}

	wui.setSxnCache(zid, wui.engine.Rework(t, env))
	return t, nil
}
func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) {
	ztl, err := wui.box.GetZettel(ctx, zid)
	if err != nil {
		return nil, err
	}

	reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()), sxreader.WithSymbolFactory(wui.sf))


	return reader, nil
}

func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env sxeval.Environment) (sx.Object, error) {
	templateExpr, err := wui.getSxnTemplate(ctx, zid, env)
	if err != nil {
		return nil, err
	}
	return wui.engine.Execute(templateExpr, env, nil)
}

func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env sxeval.Environment) error {
	return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, env)
}
func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, env sxeval.Environment) error {




	detailObj, err := wui.evalSxnTemplate(ctx, templateID, env)
	if err != nil {
		return err
	}
	env.Bind(wui.symDetail, detailObj)

	pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, env)
	if err != nil {
		return err
	}
	wui.log.Debug().Str("page", pageObj.Repr()).Msg("render")

	gen := sxhtml.NewGenerator(wui.sf, sxhtml.WithNewline)
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	if _, err = w.Write(sb.Bytes()); err != nil {
		wui.log.Error().Err(err).Msg("Unable to write HTML via template")
	}
	return nil // No error reporting, since we do not know what happended during write to client.
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	} else {
		wui.log.Debug().Err(err).Msg("reportError")
	}
	user := server.GetUser(ctx)
	env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user)
	rb.bindString("heading", sx.String(http.StatusText(code)))
	rb.bindString("message", sx.String(text))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ErrorTemplateZid, env)
	}
	errSx := rb.err
	if errSx == nil {
		return
	}
	wui.log.Error().Err(errSx).Msg("while rendering error message")


	// if errBind != nil, the HTTP header was not written
	wui.prepareAndWriteHeader(w, http.StatusInternalServerError)
	fmt.Fprintf(
		w,
		`<!DOCTYPE html>
<html>
<head><title>Internal server error</title></head>
<body>
<h1>Internal server error</h1>
<p>When generating error code %d with message:</p><pre>%v</pre><p>an error occured:</p><pre>%v</pre>
</body>
</html>`, code, text, errSx)
}

func makeStringList(sl []string) *sx.Pair {
	if len(sl) == 0 {
		return nil
	}
	result := sx.Nil()
	for i := len(sl) - 1; i >= 0; i-- {
		result = result.Cons(sx.String(sl[i]))
	}
	return result
}

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

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

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"net/http"
	"strings"
	"sync"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"







<







10
11
12
13
14
15
16

17
18
19
20
21
22
23

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"net/http"

	"sync"
	"time"

	"zettelstore.de/client.fossil/api"
	"zettelstore.de/sx.fossil"
	"zettelstore.de/sx.fossil/sxeval"
	"zettelstore.de/sx.fossil/sxhtml"
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72



73
74
75
76
77
78
79
80
81

82
83



84
85
86

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


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

125
126
127
128
129
130
131
132
133
134
135

136
137
138
139
140
141
142
143
144
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
	policy   auth.Policy

	evalZettel *usecase.Evaluate

	mxCache       sync.RWMutex
	templateCache map[id.Zid]sxeval.Expr

	mxRoleCSSMap sync.RWMutex
	roleCSSMap   map[string]id.Zid

	tokenLifetime time.Duration
	cssBaseURL    string
	cssUserURL    string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	refreshURL    string
	withAuth      bool
	loginURL      string
	logoutURL     string
	searchURL     string
	createNewURL  string

	sf      sx.SymbolFactory
	engine  *sxeval.Engine



	genHTML *sxhtml.Generator

	symQuote, symQQ *sx.Symbol
	symUQ, symUQS   *sx.Symbol
	symMetaHeader   *sx.Symbol
	symDetail       *sx.Symbol
	symA, symHref   *sx.Symbol
	symSpan         *sx.Symbol
	symAttr         *sx.Symbol

}




type webuiBox interface {
	CanCreateZettel(ctx context.Context) bool
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)

	CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool
	AllowRenameZettel(ctx context.Context, zid id.Zid) bool
	CanDeleteZettel(ctx context.Context, zid id.Zid) bool
}

// New creates a new WebUI struct.
func New(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')
	sf := sx.MakeMappedFactory()

	wui := &WebUI{
		log:      log,
		debug:    kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,

		evalZettel: evalZettel,



		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration),
		cssBaseURL:    ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(),
		cssUserURL:    ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(),
		homeURL:       ab.NewURLBuilder('/').String(),
		listZettelURL: ab.NewURLBuilder('h').String(),
		listRolesURL:  ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(),
		listTagsURL:   ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(),
		refreshURL:    ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(),
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),

		sf:            sf,

		genHTML:       sxhtml.NewGenerator(sf, sxhtml.WithNewline),
		symQuote:      sf.MustMake("quote"),
		symQQ:         sf.MustMake("quasiquote"),
		symUQ:         sf.MustMake("unquote"),
		symUQS:        sf.MustMake("unquote-splicing"),
		symDetail:     sf.MustMake("DETAIL"),
		symMetaHeader: sf.MustMake("META-HEADER"),
		symA:          sf.MustMake("a"),
		symHref:       sf.MustMake("href"),
		symSpan:       sf.MustMake("span"),
		symAttr:       sf.MustMake(sxhtml.NameSymAttr),

	}
	wui.engine = wui.createRenderEngine()
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) observe(ci box.UpdateInfo) {
	wui.mxCache.Lock()
	if ci.Reason == box.OnReload {
		wui.templateCache = make(map[id.Zid]sxeval.Expr, len(wui.templateCache))
	} else {
		delete(wui.templateCache, ci.Zid)
	}
	wui.mxCache.Unlock()

	wui.mxRoleCSSMap.Lock()
	if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid {

		wui.roleCSSMap = nil
	}
	wui.mxRoleCSSMap.Unlock()
}

func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) {
	wui.mxCache.Lock()
	wui.templateCache[zid] = expr
	wui.mxCache.Unlock()
}
func (wui *WebUI) getSxnCache(zid id.Zid) sxeval.Expr {
	wui.mxCache.RLock()
	expr, found := wui.templateCache[zid]
	wui.mxCache.RUnlock()
	if found {
		return expr
	}
	return nil
}

func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m *meta.Meta) (id.Zid, error) {
	wui.mxRoleCSSMap.RLock()
	if wui.roleCSSMap == nil {
		wui.mxRoleCSSMap.RUnlock()
		wui.mxRoleCSSMap.Lock()
		zMap, err := wui.box.GetZettel(ctx, id.RoleCSSMapZid)
		if err == nil {
			wui.roleCSSMap = createRoleCSSMap(zMap.Meta)
		}
		wui.mxRoleCSSMap.Unlock()
		if err != nil {
			return id.Invalid, err
		}
		wui.mxRoleCSSMap.RLock()
	}

	defer wui.mxRoleCSSMap.RUnlock()
	if role, found := m.Get("css-role"); found {
		if result, found2 := wui.roleCSSMap[role]; found2 {
			return result, nil
		}
	}
	if role, found := m.Get(api.KeyRole); found {
		if result, found2 := wui.roleCSSMap[role]; found2 {
			return result, nil
		}
	}
	return id.Invalid, nil
}

func createRoleCSSMap(mMap *meta.Meta) map[string]id.Zid {
	result := make(map[string]id.Zid)
	for _, p := range mMap.PairsRest() {
		key := p.Key
		if len(key) < 9 || !strings.HasPrefix(key, "css-") || !strings.HasSuffix(key, "-zid") {
			continue
		}
		zid, err2 := id.Parse(p.Value)
		if err2 != nil {
			continue
		}
		result[key[4:len(key)-4]] = zid
	}
	return result
}

func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool {
	m := meta.New(id.Invalid)
	return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx)
}

func (wui *WebUI) canWrite(
	ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool {







<
<
<














|
|
>
>
>
|

<
<
|
|
|
|
|
>


>
>
>

|
|
>
|
|
|






|












>
>
















>

<
<
<
<






>










|





|
|
>
|

|

















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







46
47
48
49
50
51
52



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


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130




131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
	policy   auth.Policy

	evalZettel *usecase.Evaluate

	mxCache       sync.RWMutex
	templateCache map[id.Zid]sxeval.Expr




	tokenLifetime time.Duration
	cssBaseURL    string
	cssUserURL    string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	refreshURL    string
	withAuth      bool
	loginURL      string
	logoutURL     string
	searchURL     string
	createNewURL  string

	sf          sx.SymbolFactory
	engine      *sxeval.Engine
	mxZettelEnv sync.Mutex
	zettelEnv   sxeval.Environment
	dag         id.Digraph
	genHTML     *sxhtml.Generator



	symMetaHeader *sx.Symbol
	symDetail     *sx.Symbol
	symA, symHref *sx.Symbol
	symSpan       *sx.Symbol
	symAttr       *sx.Symbol
	symAttrOpen   *sx.Symbol
}

// webuiBox contains all box methods that are needed for WebUI operation.
//
// Note: these function must not do auth checking.
type webuiBox interface {
	CanCreateZettel(context.Context) bool
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	CanUpdateZettel(context.Context, zettel.Zettel) bool
	AllowRenameZettel(context.Context, id.Zid) bool
	CanDeleteZettel(context.Context, id.Zid) bool
}

// New creates a new WebUI struct.
func New(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')
	sf := sx.MakeMappedFactory(256)

	wui := &WebUI{
		log:      log,
		debug:    kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,

		evalZettel: evalZettel,

		templateCache: make(map[id.Zid]sxeval.Expr, 32),

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration),
		cssBaseURL:    ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(),
		cssUserURL:    ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(),
		homeURL:       ab.NewURLBuilder('/').String(),
		listZettelURL: ab.NewURLBuilder('h').String(),
		listRolesURL:  ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(),
		listTagsURL:   ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(),
		refreshURL:    ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(),
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),

		sf:            sf,
		zettelEnv:     nil,
		genHTML:       sxhtml.NewGenerator(sf, sxhtml.WithNewline),




		symDetail:     sf.MustMake("DETAIL"),
		symMetaHeader: sf.MustMake("META-HEADER"),
		symA:          sf.MustMake("a"),
		symHref:       sf.MustMake("href"),
		symSpan:       sf.MustMake("span"),
		symAttr:       sf.MustMake(sxhtml.NameSymAttr),
		symAttrOpen:   sf.MustMake("open"),
	}
	wui.engine = wui.createRenderEngine()
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) observe(ci box.UpdateInfo) {
	wui.mxCache.Lock()
	if ci.Reason == box.OnReload {
		clear(wui.templateCache)
	} else {
		delete(wui.templateCache, ci.Zid)
	}
	wui.mxCache.Unlock()

	wui.mxZettelEnv.Lock()
	if ci.Reason == box.OnReload || wui.dag.HasVertex(ci.Zid) {
		wui.zettelEnv = nil
		wui.dag = nil
	}
	wui.mxZettelEnv.Unlock()
}

func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) {
	wui.mxCache.Lock()
	wui.templateCache[zid] = expr
	wui.mxCache.Unlock()
}
func (wui *WebUI) getSxnCache(zid id.Zid) sxeval.Expr {
	wui.mxCache.RLock()
	expr, found := wui.templateCache[zid]
	wui.mxCache.RUnlock()
	if found {
		return expr
	}
	return nil
}















































func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool {
	m := meta.New(id.Invalid)
	return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx)
}

func (wui *WebUI) canWrite(
	ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool {
236
237
238
239
240
241
242
243


244
245
246
247
248
249
250
	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)
}

func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return wui.createGenerator(wui) }



// GetURLPrefix returns the configured URL prefix of the web server.
func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() }

// NewURLBuilder creates a new URL builder object with the given key.
func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) }








|
>
>







193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
	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)
}

func (wui *WebUI) getSimpleHTMLEncoder(lang string) *htmlGenerator {
	return wui.createGenerator(wui, lang)
}

// GetURLPrefix returns the configured URL prefix of the web server.
func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() }

// NewURLBuilder creates a new URL builder object with the given key.
func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) }

Changes to web/content/content.go.

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

const (
	UnknownMIME  = "application/octet-stream"
	mimeGIF      = "image/gif"
	mimeHTML     = "text/html; charset=utf-8"
	mimeJPEG     = "image/jpeg"
	mimeMarkdown = "text/markdown; charset=utf-8"
	JSON         = "application/json"
	PlainText    = "text/plain; charset=utf-8"
	mimePNG      = "image/png"
	SXPF         = PlainText
	mimeWEBP     = "image/webp"
)

var encoding2mime = map[api.EncodingEnum]string{







<







23
24
25
26
27
28
29

30
31
32
33
34
35
36

const (
	UnknownMIME  = "application/octet-stream"
	mimeGIF      = "image/gif"
	mimeHTML     = "text/html; charset=utf-8"
	mimeJPEG     = "image/jpeg"
	mimeMarkdown = "text/markdown; charset=utf-8"

	PlainText    = "text/plain; charset=utf-8"
	mimePNG      = "image/png"
	SXPF         = PlainText
	mimeWEBP     = "image/webp"
)

var encoding2mime = map[api.EncodingEnum]string{

Changes to web/server/impl/router.go.

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
func (rt *httpRouter) Handle(pattern string, handler http.Handler) {
	rt.mux.Handle(pattern, handler)
}

func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Something may panic. Ensure a kernel log.
	defer func() {
		if reco := recover(); reco != nil {
			rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context")
			kernel.Main.LogRecover("Web", reco)
		}
	}()

	var withDebug bool
	if msg := rt.log.Debug(); msg.Enabled() {
		withDebug = true
		w = &traceResponseWriter{original: w}







|

|







104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
func (rt *httpRouter) Handle(pattern string, handler http.Handler) {
	rt.mux.Handle(pattern, handler)
}

func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Something may panic. Ensure a kernel log.
	defer func() {
		if ri := recover(); ri != nil {
			rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context")
			kernel.Main.LogRecover("Web", ri)
		}
	}()

	var withDebug bool
	if msg := rt.log.Debug(); msg.Enabled() {
		withDebug = true
		w = &traceResponseWriter{original: w}

Changes to www/build.md.

9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),
* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).

See folder `docs/development` (a zettel box) for details.

## Clone the repository
Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki).


1. Create a directory to store your Fossil repositories.
   Let's assume, you have created `$HOME/fossils`.
1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`.
1. Create a working directory.
   Let's assume, you have created `$HOME/zettelstore`.
1. Change into this directory: `cd $HOME/zettelstore`.







|
>







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),
* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).

See folder `docs/development` (a zettel box) for details.

## Clone the repository
Most of this is covered by the excellent Fossil
[documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki).

1. Create a directory to store your Fossil repositories.
   Let's assume, you have created `$HOME/fossils`.
1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`.
1. Create a working directory.
   Let's assume, you have created `$HOME/zettelstore`.
1. Change into this directory: `cd $HOME/zettelstore`.
61
62
63
64
65
66
67
68


69
70
71


72

73

74
75

76

77

```

## A note on the use of Fossil
Zettelstore is managed by the Fossil version control system.
Fossil is an alternative to the ubiquitous Git version control system.
However, Go seems to prefer Git and popular platforms that just support Git.

Some dependencies of Zettelstore, namely [Zettelstore client](https://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are also managed by Fossil.


Depending on your development setup, some error messages might occur.

If the error message mentions an environment variable called `GOVCS` you should set it to the value `GOVCS=zezzelstore.de:fossil` (alternatively more generous to `GOVCS=*:all`).


Since the Go build system is coupled with Git and some special platforms, you allow ot to download a Fossil repository from the host `zettelstore.de`.

The build tool set `GOVCS` to the right value, but you may use other `go` commands that try to download a Fossil repository.


On some operating systems, namely Termux on Android, an error message might state that an user cannot be determined (`cannot determine user`).

In this case, Fossil is allowed to download the repository, but cannot associate it with an user name.

Set the environment variable `USER` to any user name, like: `USER=nobody go run tools/build.go build`.








|
>
>


|
>
>
|
>
|
>

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

## A note on the use of Fossil
Zettelstore is managed by the Fossil version control system.
Fossil is an alternative to the ubiquitous Git version control system.
However, Go seems to prefer Git and popular platforms that just support Git.

Some dependencies of Zettelstore, namely [Zettelstore
client](https://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are
also managed by Fossil.
Depending on your development setup, some error messages might occur.

If the error message mentions an environment variable called `GOVCS` you should
set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous
to `GOVCS=*:all`).
Since the Go build system is coupled with Git and some special platforms, you
allow ot to download a Fossil repository from the host `zettelstore.de`.
The build tool set `GOVCS` to the right value, but you may use other `go`
commands that try to download a Fossil repository.

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

Changes to www/changes.wiki.

1
2






































































3
4





















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

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<title>Change Log</title>







































































<a id="0_14"></a>
<h2>Changes for Version 0.14.0 (pending)</h2>






















<a id="0_13"></a>
<h2>Changes for Version 0.13.0 (2023-08-07)</h2>
  *  There are for new search operators: less, not less, greater, not greater.
     These use the same syntax as the operators prefix, not prefix, suffix, not
     suffix. The latter are no denoted as <tt>[</tt>, <tt>![</tt>, <tt>]</tt>,
     and <tt>!]</tt>. The first may operate numerically for metadata like
     numbers, timestamps, and zettel identifier. They are not supported for
     full-test search.
     (breaking: api, webui)
  *  The API endpoint <tt>/o/{ID}</tt> (order of zettel ID) is no longer
     available. Please use the query expression <tt>{ID} ITEMS</tt> instead.

     (breaking: api)
  *  The API endpoint <tt>/u/{ID}</tt> (unlinked references of zettel ID) is no
     longer available. Please use the query expression <tt>{ID} UNLINKED</tt>
     instead.
     (breaking: api)
  *  All API endpoints allow to encode zettel data with the <tt>data</tt>
     encodings, incl. creating, updating, retrieving, and querying zettel.
     (major: api)
  *  Change syntax for context query to <tt>zid ... CONTEXT</tt>. This will
     allow to add more directives that operate on zettel identifier. Old syntax
     <tt>CONTEXT zid</tt> will be removed in 0.14.
     (major, deprecated)
  *  Add query directive <tt>ITEMS</tt> that will produce a list of metadata
     of all zettel that are referenced by the originating zettel in a top-level
     list. It replaces the API endpoint <tt>/o/{ID}</tt> (and makes it more
     useful).
     (major: api, webui)
  *  Add query directive <tt>UNLINKED</tt> that will produce a list of metadata
     of all zettel that are mentioning the originating zettel in a top-level,
     but do not mention them. It replaces the API endpoint <tt>/u/{ID}</tt>
     (and makes it more useful).
     (major: api, webui)
  *  Add query directive <tt>IDENT</tt> to distinguish a search for a zettel
     identifier (&ldquo;{ID}&rdquo;), that will list all metadata of zettel
     containing that zettel identifier, and a request to just list the metadata
     of given zettel (&ldquo;{ID} IDENT&rdquo;). The latter could be filtered
     further.
     (minor: api, webui)
  *  Add support for metadata key <tt>folge-role</tt>.
     (minor)
  *  Allow to create a child from a given zettel.
     (minor: webui)
  *  Make zettel entry/edit form a little friendlier: auto-prepend missing '#'
     to tags; ensure that role and syntax receive just a word.
     (minor: webui)
  *  Use a zettel that defines builtins for evaluating WebUI templates.
     (minor: webui)
  *  Add links to retrieve result of a query in other formats.
     (minor: webui)
  *  Always log the found configuration file.
     (minor: server)
  *  The use of the <tt>json</tt> zettel encoding is deprecated (since version
     0.12.0). Support for this encoding will be removed in version 0.14.0.
     Please use the new <tt>data</tt> encoding instead.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_12"></a>
<h2>Changes for Version 0.12.0 (2023-06-05)</h2>
  *  Syntax of templates for the web user interface are changed from Mustache


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

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





|
|
|
|

|
|
>

|
|
|

|


|

|

|
|
|
|

|
|
|
|

|
|
|
|
|

|












|
|
|







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

<a id="0_17"></a>
<h2>Changes for Version 0.17.0 (pending)</h2>

<a id="0_16"></a>
<h2>Changes for Version 0.16.0 (2023-11-30)</h2>
  *  Sx function <code>define</code> is removed, as announced for version
     0.15.0. Use <code>defvar</code> (to define variables) or
     <code>defun</code> (to define functions) instead. In addition
     <code>defunconst</code> defines a constant function, which ensures a fixed
     binding of its name to its function body (performance optimization).
     (breaking: webui)
  *  Allow to determine a role zettel for a given role.
     (major: api, webui)
  *  Present user the option to create a (missing) role zettel (in list view).
     Results in a new predefined zettel with identifier 00000000090004, which
     is a template for new role zettel.
     (minor: webui)
  *  Timestamp values can be abbrevated by omitting most of its components.
     Previously, such values that are not in the format YYYYMMDDhhmmss were
     ignored. Now the following formats are also allowed: YYYY, YYYYMM,
     YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm. Querying and sorting work accordingly.
     Previously, only a sequences of zeroes were appended, resulting in illegal
     timestamps, e.g. for YYYY or YYYYMM.
     (minor)
  *  SHTML encoder fixed w.r.t inline quoting. Previously, an &lt;q&gt; tag was
     used, which is inappropriate. Restored smart quotes from version 0.3, but
     with new SxHTML infrastructure. This affect the html encoder and the WebUI
     too. Now, an empty quote should not result in a warning by HTML linters.
     (minor: api, webui)
  *  Add new zettelmarkup inline formatting: <code>##Text##</code> will mark /
     highlight the given Text. It is typically used to highlight some text,
     which is important for you, but not for the original author. When rendered
     as HTML, the &lt;mark&gt; tag is used.
     (minor: zettelmarkup)
  *  Add configuration keys to show, not to show, or show the closed list of
     referencing zettel in the web user interface. You can set these
     configurations system-wide, per user, or per zettel. Often it is used to
     ensure a &ldquo;clean&rdquo; home zettel. Affects the list of incoming
     / back links, folge zettel, subordinate zettel, and successor zettel.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_15"></a>
<h2>Changes for Version 0.15.0 (2023-10-26)</h2>
  *  Sx function <code>define</code> is now deprecated. It will be removed in
     version 0.16. Use <code>defvar</code> or <code>defun</code> instead.
     Otherwise the WebUI will not work in version 0.16.
     (major: webui, deprecated)
  *  Zettel can be re-indexed via WebUI or API query action
     <code>REINDEX</code>. The info page of a zettel contains a link to
     re-index the zettel. In a query transclusion, this action is ignored.
     (major: api, webui).
  *  Allow to determine a tag zettel for a given tag.
     (major: api, webui)
  *  Present user the option to create a (missing) tag zettel (in list view).
     Results in a new predefined zettel with identifier 00000000090003, which
     is a template for new tag zettel.
     (minor: webui)
  *  ZIP file with manual now contains a zettel 00001000000000 that contains
     its build date (metadata key <code>created</code>) and version (in the
     zettel content)
     (minor)
  *  If an error page cannot be created due to template errors (or similar), a
     plain text error page is delivered instead. It shows the original error
     and the error that occured durng rendering the original error page.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_14"></a>
<h2>Changes for Version 0.14.0 (2023-09-22)</h2>
  *  Remove support for JSON. This was marked deprecated in version 0.12.0. Use
     the <code>data</code> encoding instead, a form of symbolic expressions.
     (breaking: api; minor: webui)
  *  Remove deprecated syntax for a context list: <code>CONTEXT zid</code>. Use
     <code>zid CONTEXT</code> instead. It was deprecated in version 0.13.0.
     (breaking: api, webui, zettelmarkup)
  *  Replace CSS-role-map mechanism with a more general Sx-based one: user
     specific code may generates parts of resulting HTML document.
     (breaking: webui)
  *  Allow meta-tags, i.e. zettel for a specific tag. Meta-tags have the tag
     name as a title and specify the role "tag".
     (major: webui)
  *  Allow to load sx code from multiple zettel; dependencies are specified
     using <code>precursor</code> metadata.
     (major: webui)
  *  Allow sx code to change WebUI for zettel with specified role.
     (major: webui)
  *  Some minor usability improvements.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_13"></a>
<h2>Changes for Version 0.13.0 (2023-08-07)</h2>
  *  There are for new search operators: less, not less, greater, not greater.
     These use the same syntax as the operators prefix, not prefix, suffix, not
     suffix. The latter are now denoted as <code>[</code>, <code>![</code>,
     <code>]</code>, and <code>!]</code>. The first may operate numerically for
     metadata like numbers, timestamps, and zettel identifier. They are not
     supported for full-text search.
     (breaking: api, webui)
  *  The API endpoint <code>/o/{ID}</code> (order of zettel ID) is no longer
     available. Please use the query expression <code>{ID} ITEMS</code>
     instead.
     (breaking: api)
  *  The API endpoint <code>/u/{ID}</code> (unlinked references of zettel ID)
     is no longer available. Please use the query expression <code>{ID}
     UNLINKED</code> instead.
     (breaking: api)
  *  All API endpoints allow to encode zettel data with the <code>data</code>
     encodings, incl. creating, updating, retrieving, and querying zettel.
     (major: api)
  *  Change syntax for context query to <code>zid ... CONTEXT</code>. This will
     allow to add more directives that operate on zettel identifier. Old syntax
     <code>CONTEXT zid</code> will be removed in 0.14.
     (major, deprecated)
  *  Add query directive <code>ITEMS</code> that will produce a list of
     metadata of all zettel that are referenced by the originating zettel in
     a top-level list. It replaces the API endpoint <code>/o/{ID}</code> (and
     makes it more useful).
     (major: api, webui)
  *  Add query directive <code>UNLINKED</code> that will produce a list of
     metadata of all zettel that are mentioning the originating zettel in
     a top-level, but do not mention them. It replaces the API endpoint
     <code>/u/{ID}</code> (and makes it more useful).
     (major: api, webui)
  *  Add query directive <code>IDENT</code> to distinguish a search for
     a zettel identifier (&ldquo;{ID}&rdquo;), that will list all metadata of
     zettel containing that zettel identifier, and a request to just list the
     metadata of given zettel (&ldquo;{ID} IDENT&rdquo;). The latter could be
     filtered further.
     (minor: api, webui)
  *  Add support for metadata key <code>folge-role</code>.
     (minor)
  *  Allow to create a child from a given zettel.
     (minor: webui)
  *  Make zettel entry/edit form a little friendlier: auto-prepend missing '#'
     to tags; ensure that role and syntax receive just a word.
     (minor: webui)
  *  Use a zettel that defines builtins for evaluating WebUI templates.
     (minor: webui)
  *  Add links to retrieve result of a query in other formats.
     (minor: webui)
  *  Always log the found configuration file.
     (minor: server)
  *  The use of the <code>json</code> zettel encoding is deprecated (since
     version 0.12.0). Support for this encoding will be removed in version
     0.14.0. Please use the new <code>data</code> encoding instead.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_12"></a>
<h2>Changes for Version 0.12.0 (2023-06-05)</h2>
  *  Syntax of templates for the web user interface are changed from Mustache
79
80
81
82
83
84
85
86
87
88
89

90
91
92
93
94
95

96
97

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
     that is the short form for "symbolic expression for HTML".
     (breaking)
  *  Render footer zettel on all WebUI pages.
     (fix: webui)
  *  Query search operator "=" now compares for equality, ":" compares
     depending on the value type.
     (minor: api, webui)
  *  Search term <tt>PICK</tt> now respects the original sort order. This makes
     it more useful and orthogonal to <tt>RANDOM</tt> and <tt>LIMIT</tt>. As
     a side effect, zettel lists retrieved via the API are no longer sorted. In
     case you want a specific order, you must specify it explicit.

     (minor: api, webui)
  *  New metadata key <tt>expire</tt> records a timestamp when a zettel should
     be treated as, well, expired.
     (minor)
  *  New metadata keys <tt>superior</tt> and <tt>subordinate</tt> (calculated
     from <tt>superior</tt>) allow to specify a hierarchy between zettel.

     (minor)
  *  Metadata keys with suffix <tt>-date</tt> and <tt>-time</tt> are treated as

     timestamp values.
     (minor)
  *  <tt>sexpr</tt> zettel encoding is now documented in the manual.
     (minor: manual)
  *  Build tool allows to install / update external Go tools needed to build
     the software.
     (minor)
  *  Show only useful metadata on WebUI, not the internal metadata.
     (minor: webui)
  *  The use of the <tt>json</tt> zettel encoding is deprecated. Support for
     this encoding may be removed in future versions. Please use the new
     <tt>data</tt> encoding instead.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_11"></a>
<h2>Changes for Version 0.11.2 (2023-04-16)</h2>
  *  Render footer zettel on all WebUI pages.
     Backported from 0.12.0.
     Many thanks to HK for reporting it!
     (fix: webui)

<h2>Changes for Version 0.11.1 (2023-03-28)</h2>
  *  Make <tt>PICK</tt> search term a little bit more deterministic so that the
     &ldquo;Save As Zettel&rdquo; button produces the same list.
     (fix: webui)

<h2>Changes for Version 0.11.0 (2023-03-27)</h2>
  *  Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
     encoding instead.
     (breaking)
  *  Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
     just a plain string, possibly empty. Therefore, no inline formatting (like
     bold text), no links, no footnotes, no citations (the latter made
     rendering the title often questionable, in some contexts). If you used
     special entities, please use the unicode characters directly. However, as
     a good practice, it is often the best to printable ASCII characters.
     (breaking)
  *  Remove runtime configuration <tt>marker-external</tt>. It was added in
     version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
     change the marker for an external URL, you could modify zettel
     00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
     (Zettelstore User CSS, preferred) by changing / adding a rule to add some
     content after an external <tt><a ...></tt> tag.
     (breaking: webui)
  *  Add SHTML encoding. This allows to ensure the quality of generated HTML
     code. In addition, clients might use it, because it is easier to parse and
     manipulate than ordinary HTML. In the future, HTML template zettel will
     probably also use SHTML, deprecating the current Mustache syntax (which
     was added in [#0_0_9|0.0.9]).
     (major)
  *  Search term <tt>PICK n</tt>, where <tt>n</tt> is an integer value greater
     zero, will pick randomly <tt>n</tt> elements from the search result list.
     Somehow similar (and faster) as <tt>RANDOM LIMIT n</tt>, but allows also
     later ordering of the resulting list.
     (minor)
  *  Changed cost model for zettel context: a zettel with more
     outgoing/incoming references has higher cost than a zettel with less
     references. Also added support for traversing tags, with a similar cost
     model. As an effect, zettel hubs (in many cases your home zettel) will
     less likely add its references. Same for often used tags. The cost model
     might change in some details in the future, but the idea of a penalty
     applied to zettel / tags with many references will hold.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_10"></a>
<h2>Changes for Version 0.10.1 (2023-01-30)</h2>
  *  Show button to save a query into a zettel only when the current user has
     authorization to do it.
     (fix: webui)

<h2>Changes for Version 0.10.0 (2023-01-24)</h2>
  *  Remove support for endpoints <tt>/j, /m, /q, /p, /v</tt>. Their functions
     are merged into endpoint <tt>/z</tt>. This was announced in version 0.9.0.
     Please use only client library with at least version 0.10.0 too.

     (breaking: api)
  *  Remove support for runtime configuration key <tt>footer-html</tt>. Use
     <tt>footer-zettel</tt> instead. Deprecated in version 0.9.0.
     (breaking: webui)
  *  Save a query into a zettel to freeze it.
     (major: webui)
  *  Allow to show all used metadata keys, linked with their occurrences and
     their values.
     (minor: webui)
  *  Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding
     instead.
     (deprecated)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_9"></a>
<h2>Changes for Version 0.9.0 (2022-12-12)</h2>
  *  Remove support syntax <tt>pikchr</tt>. Although it was a nice idea to
     include it into Zettelstore, the implementation is too brittle (w.r.t. the
     expected long lifetime of Zettelstore). There should be other ways to
     support SVG front-ends.
     (breaking)
  *  Allow to upload content when creating / updating a zettel.
     (major: webui)
  *  Add syntax &ldquo;draw&rdquo; (again)
     (minor: zettelmarkup)
  *  Allow to encode zettel in Markdown. Please note: not every aspect of
     a zettel can be encoded in Markdown. Those aspects will be ignored.
     (minor: api)
  *  Enhance zettel context by raising the importance of folge zettel (and
     similar).
     (minor: api, webui)
  *  Interpret zettel files with extension <tt>.webp</tt> as an binary image
     file format.
     (minor)
  *  Allow to specify service specific log level via statup configuration and
     via command line.
     (minor)
  *  Allow to specify a zettel to serve footer content via runtime
     comfiguration <tt>footer-zettel</tt>. Can be overwritten by user zettel.

     (minor: webui)
  *  Footer data is automatically separated by a thematic break / horizontal
     rule. If you do not like it, you have to update the base template.
     (minor: webui)
  *  Allow to set runtime configuration <tt>home-zettel</tt> in the user zettel
     to make it user-specific.
     (minor: webui)
  *  Serve favicon.ico from the asset directory.
     (minor: webui)
  *  Zettelmarkup cheat sheet
     (minor: manual)
  *  Runtime configuration key <tt>footer-html</tt> will be removed in Version
     0.10.0. Please use <tt>footer-zettel</tt> instead.
     (deprecated: webui)
  *  In the next version 0.10.0, the API endpoints for a zettel (<tt>/j</tt>,
     <tt>/p</tt>, <tt>/v</tt>) will be merged with endpoint <tt>/z</tt>. Basically,

     the previous endpoint will be refactored as query parameter of endpoint
     <tt>/z</tt>. To reduce errors, there will be no version, where the previous
     endpoint are still available and the new funnctionality is still there.
     This is a warning to prepare for some breaking changes in v0.10.0. This
     also affects the API client implementation.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_8"></a>
<h2>Changes for Version 0.8.0 (2022-10-20)</h2>
  *  Remove support for tags within zettel content. Removes also property
     metadata keys <tt>all-tags</tt> and <tt>computed-tags</tt>. Deprecated in
     version 0.7.0.
     (breaking: zettelmarkup, api, webui)
  *  Remove API endpoint <tt>/m</tt>, which retrieve aggregated (tags, roles)
     zettel identifier. Deprecated in version 0.7.0.
     (breaking: api)
  *  Remove support for URL query parameter starting with an underscore.
     Deprecated in version 0.7.0.
     (breaking: api, webui)
  *  Ignore HTML content by default, and allow HTML gradually by setting
     startup value <tt>insecure-html</tt>.
     (breaking: markup)
  *  Endpoint <tt>/q</tt> returns list of full metadata, if no query action is
     specified. A HTTP call <tt>GET /z</tt> (retrieving metadata of all or some
     zettel) is now an alias for <tt>GET /q</tt>.
     (major: api)
  *  Allow to create a zettel that acts as the new version of an existing
     zettel. Useful if you want to have access to older, outdated content.
     (minor: webui)
  *  Allow transclusion to reference local image via URL.
     (minor: zettelmarkup, webui)
  *  Add categories in RSS feed, based on zettel tags.







|
|
|
|
>

|
|

|
|
>

|
>


|






|
|
|






|
<
|



|
|













|




|







|
|
|
|



















|
|
|
>

|
|














|














|
|





|
>




|
|





|
|

|
|
>
|
|
|
|
|







|
|

|
|





|

|
|
|







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
     that is the short form for "symbolic expression for HTML".
     (breaking)
  *  Render footer zettel on all WebUI pages.
     (fix: webui)
  *  Query search operator "=" now compares for equality, ":" compares
     depending on the value type.
     (minor: api, webui)
  *  Search term <code>PICK</code> now respects the original sort order. This
     makes it more useful and orthogonal to <code>RANDOM</code> and
     <code>LIMIT</code>. As a side effect, zettel lists retrieved via the API
     are no longer sorted. In case you want a specific order, you must specify
     it explicit.
     (minor: api, webui)
  *  New metadata key <code>expire</code> records a timestamp when a zettel
     should be treated as, well, expired.
     (minor)
  *  New metadata keys <code>superior</code> and <code>subordinate</code>
     (calculated from <code>superior</code>) allow to specify a hierarchy
     between zettel.
     (minor)
  *  Metadata keys with suffix <code>-date</code> and <code>-time</code> are
     treated as
     timestamp values.
     (minor)
  *  <code>sexpr</code> zettel encoding is now documented in the manual.
     (minor: manual)
  *  Build tool allows to install / update external Go tools needed to build
     the software.
     (minor)
  *  Show only useful metadata on WebUI, not the internal metadata.
     (minor: webui)
  *  The use of the <code>json</code> zettel encoding is deprecated. Support
     for this encoding may be removed in future versions. Please use the new
     <code>data</code> encoding instead.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_11"></a>
<h2>Changes for Version 0.11.2 (2023-04-16)</h2>
  *  Render footer zettel on all WebUI pages. Backported from 0.12.0. Many

     thanks to HK for reporting it!
     (fix: webui)

<h2>Changes for Version 0.11.1 (2023-03-28)</h2>
  *  Make <code>PICK</code> search term a little bit more deterministic so that
     the &ldquo;Save As Zettel&rdquo; button produces the same list.
     (fix: webui)

<h2>Changes for Version 0.11.0 (2023-03-27)</h2>
  *  Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
     encoding instead.
     (breaking)
  *  Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
     just a plain string, possibly empty. Therefore, no inline formatting (like
     bold text), no links, no footnotes, no citations (the latter made
     rendering the title often questionable, in some contexts). If you used
     special entities, please use the unicode characters directly. However, as
     a good practice, it is often the best to printable ASCII characters.
     (breaking)
  *  Remove runtime configuration <code>marker-external</code>. It was added in
     version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
     change the marker for an external URL, you could modify zettel
     00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
     (Zettelstore User CSS, preferred) by changing / adding a rule to add some
     content after an external <code><a ...></code> tag.
     (breaking: webui)
  *  Add SHTML encoding. This allows to ensure the quality of generated HTML
     code. In addition, clients might use it, because it is easier to parse and
     manipulate than ordinary HTML. In the future, HTML template zettel will
     probably also use SHTML, deprecating the current Mustache syntax (which
     was added in [#0_0_9|0.0.9]).
     (major)
  *  Search term <code>PICK n</code>, where <code>n</code> is an integer value
     greater zero, will pick randomly <code>n</code> elements from the search
     result list. Somehow similar (and faster) as <code>RANDOM LIMIT n</code>,
     but allows also later ordering of the resulting list.
     (minor)
  *  Changed cost model for zettel context: a zettel with more
     outgoing/incoming references has higher cost than a zettel with less
     references. Also added support for traversing tags, with a similar cost
     model. As an effect, zettel hubs (in many cases your home zettel) will
     less likely add its references. Same for often used tags. The cost model
     might change in some details in the future, but the idea of a penalty
     applied to zettel / tags with many references will hold.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_10"></a>
<h2>Changes for Version 0.10.1 (2023-01-30)</h2>
  *  Show button to save a query into a zettel only when the current user has
     authorization to do it.
     (fix: webui)

<h2>Changes for Version 0.10.0 (2023-01-24)</h2>
  *  Remove support for endpoints <code>/j, /m, /q, /p, /v</code>. Their
     functions are merged into endpoint <code>/z</code>. This was announced in
     version 0.9.0. Please use only client library with at least version 0.10.0
     too.
     (breaking: api)
  *  Remove support for runtime configuration key <code>footer-html</code>. Use
     <code>footer-zettel</code> instead. Deprecated in version 0.9.0.
     (breaking: webui)
  *  Save a query into a zettel to freeze it.
     (major: webui)
  *  Allow to show all used metadata keys, linked with their occurrences and
     their values.
     (minor: webui)
  *  Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding
     instead.
     (deprecated)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_9"></a>
<h2>Changes for Version 0.9.0 (2022-12-12)</h2>
  *  Remove support syntax <code>pikchr</code>. Although it was a nice idea to
     include it into Zettelstore, the implementation is too brittle (w.r.t. the
     expected long lifetime of Zettelstore). There should be other ways to
     support SVG front-ends.
     (breaking)
  *  Allow to upload content when creating / updating a zettel.
     (major: webui)
  *  Add syntax &ldquo;draw&rdquo; (again)
     (minor: zettelmarkup)
  *  Allow to encode zettel in Markdown. Please note: not every aspect of
     a zettel can be encoded in Markdown. Those aspects will be ignored.
     (minor: api)
  *  Enhance zettel context by raising the importance of folge zettel (and
     similar).
     (minor: api, webui)
  *  Interpret zettel files with extension <code>.webp</code> as an binary
     image file format.
     (minor)
  *  Allow to specify service specific log level via statup configuration and
     via command line.
     (minor)
  *  Allow to specify a zettel to serve footer content via runtime
     comfiguration <code>footer-zettel</code>. Can be overwritten by user
     zettel.
     (minor: webui)
  *  Footer data is automatically separated by a thematic break / horizontal
     rule. If you do not like it, you have to update the base template.
     (minor: webui)
  *  Allow to set runtime configuration <code>home-zettel</code> in the user
     zettel to make it user-specific.
     (minor: webui)
  *  Serve favicon.ico from the asset directory.
     (minor: webui)
  *  Zettelmarkup cheat sheet
     (minor: manual)
  *  Runtime configuration key <code>footer-html</code> will be removed in
     Version 0.10.0. Please use <code>footer-zettel</code> instead.
     (deprecated: webui)
  *  In the next version 0.10.0, the API endpoints for a zettel
     (<code>/j</code>, <code>/p</code>, <code>/v</code>) will be merged with
     endpoint <code>/z</code>. Basically, the previous endpoint will be
     refactored as query parameter of endpoint <code>/z</code>. To reduce
     errors, there will be no version, where the previous endpoint are still
     available and the new funnctionality is still there. This is a warning to
     prepare for some breaking changes in v0.10.0. This also affects the API
     client implementation.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_8"></a>
<h2>Changes for Version 0.8.0 (2022-10-20)</h2>
  *  Remove support for tags within zettel content. Removes also property
     metadata keys <code>all-tags</code> and <code>computed-tags</code>.
     Deprecated in version 0.7.0.
     (breaking: zettelmarkup, api, webui)
  *  Remove API endpoint <code>/m</code>, which retrieve aggregated (tags,
     roles) zettel identifier. Deprecated in version 0.7.0.
     (breaking: api)
  *  Remove support for URL query parameter starting with an underscore.
     Deprecated in version 0.7.0.
     (breaking: api, webui)
  *  Ignore HTML content by default, and allow HTML gradually by setting
     startup value <code>insecure-html</code>.
     (breaking: markup)
  *  Endpoint <code>/q</code> returns list of full metadata, if no query action
     is specified. A HTTP call <code>GET /z</code> (retrieving metadata of all
     or some zettel) is now an alias for <code>GET /q</code>.
     (major: api)
  *  Allow to create a zettel that acts as the new version of an existing
     zettel. Useful if you want to have access to older, outdated content.
     (minor: webui)
  *  Allow transclusion to reference local image via URL.
     (minor: zettelmarkup, webui)
  *  Add categories in RSS feed, based on zettel tags.
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
<a id="0_7"></a>
<h2>Changes for Version 0.7.1 (2022-09-18)</h2>
  *  Produce a RSS feed compatible to Miniflux.
     (minor)
  *  Make sure to always produce a pubdata in RSS feed.
     (bug)
  *  Prefix search for data that looks like a zettel identifier may end with a
     <tt>0</tt>.
     (bug)
  *  Fix glitch on manual zettel.
     (bug)

<h2>Changes for Version 0.7.0 (2022-09-17)</h2>
  *  Removes support for URL query parameter to search for metadata values,
     sorting, offset, and limit a zettel list. Deprecated in version 0.6.0
     (breaking: api, webui)
  *  Allow to search for the existence / non-existence of a metadata key with
     the "?" operator: <tt>key?</tt> and <tt>key!?</tt>. Previously, the ":"
     operator was used for this by specifying an empty search value. Now you
     can use the ":" operator to find empty / non-empty metadata values. If you
     specify a search operator for metadata, the specified key is assumed to
     exist.
     (breaking: api, webui)
  *  Rename &ldquo;search expression&rdquo; into &ldquo;query
     expressions&rdquo;. Similar, the reference prefix <tt>search:</tt> to
     specify a query link or a query transclusion is renamed to <tt>query:</tt>

     (breaking: zettelmarkup)
  *  Rename query parameter for query expression from <tt>_s</tt> to
     <tt>q</tt>.
     (breaking: api, webui)
  *  Cleanup names for HTTP query parameters in WebUI. Update your bookmarks
     if you used them. (For API: see below)
     (breaking: webui)
  *  Allow search terms to be OR-ed. This allows to specify any search
     expression in disjunctive normal form. Therefore, the NEGATE term is not
     needed any more.
     (breaking: api, webui)
  *  Replace runtime configuration <tt>default-lang</tt> with <tt>lang</tt>.
     Additionally, <tt>lang</tt> set at the zettel of the current user, will
     provide a default value for the current user, overwriting the global
     default value.
     (breaking)
  *  Add new syntax <tt>pikchr</tt>, a markup language for diagrams in
     technical documentation.
     (major)
  *  Add endpoint <tt>/q</tt> to query the zettelstore and aggregate resulting
     values. This is done by extending the query syntax.
     (major: api)
  *  Add support for query actions. Actions may aggregate w.r.t. some metadata
     keys, or produce an RSS feed.
     (major: api, webui)
  *  Query results can be ordered for more than one metadata key. Ordering by
     zettel identifier is an implicit last order expression to produce stable
     results.
     (minor: api, webui)
  *  Add support for an asset directory, accessible via URL prefix
     <tt>/assests/</tt>.
     (minor: server)
  *  Add support for metadata key <tt>created</tt>, a timestamp when the zettel
     was created. Since key <tt>published</tt> is now either <tt>created</tt>

     or <tt>modified</tt>, it will now always contains a valid time stamp.
     (minor)
  *  Add support for metadata key <tt>author</tt>. It will be displayed on a
     zettel, if set.
     (minor: webui)
  *  Remove CSS for lists. The browsers default value for <tt>padding-left</tt>
     will be used.
     (minor: webui)
  *  Removed templates for rendering roles and tags lists. This is now done by
     query actions.
     (minor: webui)
  *  Tags within zettel content are deprecated in version 0.8. This affects the
     computed metadata keys <tt>content-tags</tt> and <tt>all-tags</tt>. They
     will be removed. The number sign of a content tag introduces unintended
     tags, esp. in the english language; content tags may occur within links
     &rarr; links within links, when rendered as HTML; content tags may occur
     in the title of a zettel; naming of content tags, zettel tags, and their
     union is confusing for many. Migration: use zettel tags or replace content
     tag with a search.
     (deprecated: zettelmarkup)
  *  Cleanup names for HTTP query parameter for API calls. Essentially,
     underscore characters in front are removed. Please use new names, old
     names will be deprecated in version 0.8.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.







|









|
|
|
|
|


|
|
>

|
|








|
|
|
|

|


|
|









|

|
|
>
|

|
|

|
|





|
|
|
|
|
|
|







378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
<a id="0_7"></a>
<h2>Changes for Version 0.7.1 (2022-09-18)</h2>
  *  Produce a RSS feed compatible to Miniflux.
     (minor)
  *  Make sure to always produce a pubdata in RSS feed.
     (bug)
  *  Prefix search for data that looks like a zettel identifier may end with a
     <code>0</code>.
     (bug)
  *  Fix glitch on manual zettel.
     (bug)

<h2>Changes for Version 0.7.0 (2022-09-17)</h2>
  *  Removes support for URL query parameter to search for metadata values,
     sorting, offset, and limit a zettel list. Deprecated in version 0.6.0
     (breaking: api, webui)
  *  Allow to search for the existence / non-existence of a metadata key with
     the "?" operator: <code>key?</code> and <code>key!?</code>. Previously,
     the ":" operator was used for this by specifying an empty search value.
     Now you can use the ":" operator to find empty / non-empty metadata
     values. If you specify a search operator for metadata, the specified key
     is assumed to exist.
     (breaking: api, webui)
  *  Rename &ldquo;search expression&rdquo; into &ldquo;query
     expressions&rdquo;. Similar, the reference prefix <code>search:</code> to
     specify a query link or a query transclusion is renamed to
     <code>query:</code>
     (breaking: zettelmarkup)
  *  Rename query parameter for query expression from <code>_s</code> to
     <code>q</code>.
     (breaking: api, webui)
  *  Cleanup names for HTTP query parameters in WebUI. Update your bookmarks
     if you used them. (For API: see below)
     (breaking: webui)
  *  Allow search terms to be OR-ed. This allows to specify any search
     expression in disjunctive normal form. Therefore, the NEGATE term is not
     needed any more.
     (breaking: api, webui)
  *  Replace runtime configuration <code>default-lang</code> with
     <code>lang</code>. Additionally, <code>lang</code> set at the zettel of
     the current user, will provide a default value for the current user,
     overwriting the global default value.
     (breaking)
  *  Add new syntax <code>pikchr</code>, a markup language for diagrams in
     technical documentation.
     (major)
  *  Add endpoint <code>/q</code> to query the zettelstore and aggregate
     resulting values. This is done by extending the query syntax.
     (major: api)
  *  Add support for query actions. Actions may aggregate w.r.t. some metadata
     keys, or produce an RSS feed.
     (major: api, webui)
  *  Query results can be ordered for more than one metadata key. Ordering by
     zettel identifier is an implicit last order expression to produce stable
     results.
     (minor: api, webui)
  *  Add support for an asset directory, accessible via URL prefix
     <code>/assests/</code>.
     (minor: server)
  *  Add support for metadata key <code>created</code>, a timestamp when the
     zettel was created. Since key <code>published</code> is now either
     <code>created</code> or <code>modified</code>, it will now always contains
     a valid time stamp.
     (minor)
  *  Add support for metadata key <code>author</code>. It will be displayed on
     a zettel, if set.
     (minor: webui)
  *  Remove CSS for lists. The browsers default value for
     <code>padding-left</code> will be used.
     (minor: webui)
  *  Removed templates for rendering roles and tags lists. This is now done by
     query actions.
     (minor: webui)
  *  Tags within zettel content are deprecated in version 0.8. This affects the
     computed metadata keys <code>content-tags</code> and
     <code>all-tags</code>. They will be removed. The number sign of a content
     tag introduces unintended tags, esp. in the english language; content tags
     may occur within links &rarr; links within links, when rendered as HTML;
     content tags may occur in the title of a zettel; naming of content tags,
     zettel tags, and their union is confusing for many. Migration: use zettel
     tags or replace content tag with a search.
     (deprecated: zettelmarkup)
  *  Cleanup names for HTTP query parameter for API calls. Essentially,
     underscore characters in front are removed. Please use new names, old
     names will be deprecated in version 0.8.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
  *  If authentication is enabled, a secret of at least 16 bytes must be set in
     the startup configuration.
     (breaking)
  *  &ldquo;Sexpr&rdquo; encoding replaces &ldquo;Native&rdquo; encoding. Sexpr
     encoding is much easier to parse, compared with native and ZJSON encoding.
     In most cases it is smaller than ZJSON.
     (breaking: api)
  *  Endpoint <tt>/r</tt> is changed to <tt>/m?_key=role</tt> and returns now
     a map of role names to the list of zettel having this role. Endpoint
     <tt>/t</tt> is changed to <tt>/m?_key=tags</tt>. It already returned
     mapping described before.
     (breaking: api)
  *  Remove support for a default value for metadata key title, role, and
     syntax. Title and role are now allowed to be empty, an empty syntax value
     defaults to &ldquo;plain&rdquo;.
     (breaking)
  *  Add support for an &ldquo;evaluation block&rdquo; syntax in Zettelmarkup
     to allow interpretation of content by external software.







|
|
|
|







523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
  *  If authentication is enabled, a secret of at least 16 bytes must be set in
     the startup configuration.
     (breaking)
  *  &ldquo;Sexpr&rdquo; encoding replaces &ldquo;Native&rdquo; encoding. Sexpr
     encoding is much easier to parse, compared with native and ZJSON encoding.
     In most cases it is smaller than ZJSON.
     (breaking: api)
  *  Endpoint <code>/r</code> is changed to <code>/m?_key=role</code> and
     returns now a map of role names to the list of zettel having this role.
     Endpoint <code>/t</code> is changed to <code>/m?_key=tags</code>. It
     already returned mapping described before.
     (breaking: api)
  *  Remove support for a default value for metadata key title, role, and
     syntax. Title and role are now allowed to be empty, an empty syntax value
     defaults to &ldquo;plain&rdquo;.
     (breaking)
  *  Add support for an &ldquo;evaluation block&rdquo; syntax in Zettelmarkup
     to allow interpretation of content by external software.
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
     documentation.

<a id="0_4"></a>
<h2>Changes for Version 0.4 (2022-03-08)</h2>
  *  Encoding &ldquo;djson&rdquo; renamed to &ldquo;zjson&rdquo; (<em>zettel
     json</em>).
     (breaking: api; minor: webui)
  *  Remove inline quotation syntax <tt>&lt;&lt;...&lt;&lt;</tt>. Now,
     <tt>&quot;&quot;...&quot;&quot;</tt> generates the equivalent code.
     Typographical quotes are generated by the browser, not by Zettelstore.
     (breaking: Zettelmarkup)
  *  Remove inline formatting for monospace. Its syntax is now used by the
     similar syntax element of literal computer input. Monospace was just
     a visual element with no semantic association. Now, the syntax
     <kbd>++...++</kbd> is obsolete.
     (breaking: Zettelmarkup).







|
|







571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
     documentation.

<a id="0_4"></a>
<h2>Changes for Version 0.4 (2022-03-08)</h2>
  *  Encoding &ldquo;djson&rdquo; renamed to &ldquo;zjson&rdquo; (<em>zettel
     json</em>).
     (breaking: api; minor: webui)
  *  Remove inline quotation syntax <code>&lt;&lt;...&lt;&lt;</code>. Now,
     <code>&quot;&quot;...&quot;&quot;</code> generates the equivalent code.
     Typographical quotes are generated by the browser, not by Zettelstore.
     (breaking: Zettelmarkup)
  *  Remove inline formatting for monospace. Its syntax is now used by the
     similar syntax element of literal computer input. Monospace was just
     a visual element with no semantic association. Now, the syntax
     <kbd>++...++</kbd> is obsolete.
     (breaking: Zettelmarkup).
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
     interpreted as Zettelmarkup. Similar, the suffix <kbd>-set</kbd> denotes
     a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel
     identifier.
     (minor: api, webui)
  *  Change generated URLs for zettel-creation forms. If you have bookmarked
     them, e.g. to create a new zettel, you should update.
     (minor: webui)
  *  Remove support for metadata key <tt>no-index</tt> to suppress indexing
     selected zettel. It was introduced in <a href="#0_0_11">v0.0.11</a>, but
     disallows some future optimizations for searching zettel.
     (minor: api, webui)
  *  Make some metadata-based searches a little bit faster by executing
     a (in-memory-based) full-text search first. Now only those zettel are
     loaded from file that contain the metdata value.
     (minor: api, webui)
  *  Add an API call to retrieve the version of the Zettelstore.
     (minor: api)
  *  Limit the amount of zettel and bytes to be stored in a memory box. Allows
     to use it with public access.
     (minor: box)
  *  Disallow to cache the authentication cookie. Will remove most unexpected
     log-outs when using a mobile device.
     (minor: webui)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_3"></a>
<h2>Changes for Version 0.3 (2022-02-09)</h2>
  *  Zettel files with extension <tt>.meta</tt> are now treated as content
     files. Previoulsy, they were interpreted as metadata files. The
     interpretation as metadata files was deprecated in version 0.2.
     (breaking: directory and file/zip box)
  *  Add syntax &ldquo;draw&rdquo; to produce some graphical representations.
     (major)
  *  Add Zettelmarkup syntax to specify full transclusion of other zettel.
     (major: Zettelmarkup)







|




















|







603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
     interpreted as Zettelmarkup. Similar, the suffix <kbd>-set</kbd> denotes
     a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel
     identifier.
     (minor: api, webui)
  *  Change generated URLs for zettel-creation forms. If you have bookmarked
     them, e.g. to create a new zettel, you should update.
     (minor: webui)
  *  Remove support for metadata key <code>no-index</code> to suppress indexing
     selected zettel. It was introduced in <a href="#0_0_11">v0.0.11</a>, but
     disallows some future optimizations for searching zettel.
     (minor: api, webui)
  *  Make some metadata-based searches a little bit faster by executing
     a (in-memory-based) full-text search first. Now only those zettel are
     loaded from file that contain the metdata value.
     (minor: api, webui)
  *  Add an API call to retrieve the version of the Zettelstore.
     (minor: api)
  *  Limit the amount of zettel and bytes to be stored in a memory box. Allows
     to use it with public access.
     (minor: box)
  *  Disallow to cache the authentication cookie. Will remove most unexpected
     log-outs when using a mobile device.
     (minor: webui)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_3"></a>
<h2>Changes for Version 0.3 (2022-02-09)</h2>
  *  Zettel files with extension <code>.meta</code> are now treated as content
     files. Previoulsy, they were interpreted as metadata files. The
     interpretation as metadata files was deprecated in version 0.2.
     (breaking: directory and file/zip box)
  *  Add syntax &ldquo;draw&rdquo; to produce some graphical representations.
     (major)
  *  Add Zettelmarkup syntax to specify full transclusion of other zettel.
     (major: Zettelmarkup)
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580

581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628

629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656

657
658
659
660
661
662
663
664
665
666
667
     (minor: directory and file/zip box)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_2"></a>
<h2>Changes for Version 0.2 (2022-01-19)</h2>
  *  v0.2.1 (2021-02-01) updates the license year in some documents
  *  Remove support for <tt>;;small text;;</tt> Zettelmarkup.
     (breaking: Zettelmarkup)
  *  On macOS, the downloadable executable program is now called
     &ldquo;zettelstore&rdquo;, as on all other Unix-like platforms.
     (possibly breaking: macOS)
  *  External metadata (e.g. for zettel with file extension other than
     <tt>.zettel</tt>) are stored in files without an extension. Metadata files
     with extension <tt>.meta</tt> are still recognized, but result in
     a warning message. In a future version (probably v0.3), <tt>.meta</tt>
     files will be treated as ordinary content files, possibly resulting in
     duplicate content. In other words: usage of <tt>.meta</tt> files for
     storing metadata is deprecated.
     (possibly breaking: directory and file box)
  *  Show unlinked references in info page of each zettel. Unlinked references
     are phrases within zettel content that might reference another zettel with
     the same title as the phase.
     (major: webui)
  *  Add endpoint <tt>/u/{ID}</tt> to retrieve unlinked references.
     (major: api)
  *  Provide a logging facility.
     Log messages are written to standard output. Messages with level
     &ldquo;information&rdquo; are also written to a circular buffer (of length
     8192) which can be retrieved via a computed zettel. There is a command
     line flag <tt>-l LEVEL</tt> to specify an application global logging level
     on startup (default: &ldquo;information&rdquo;). Logging level can also be
     changed via the administrator console, even for specific (sub-) services.

     (major)
  *  The internal handling of zettel files is rewritten. This allows less
     reloads ands detects when the directory containing the zettel files is
     removed. The API, WebUI, and the admin console allow to manually refresh
     the internal state on demand.
     (major: box, webui)
  *  <tt>.zettel</tt> files with YAML header are now correctly written.
     (bug)
  *  Selecting zettel based on their metadata allows the same syntax as
     searching for zettel content. For example, you can list all zettel that
     have an identifier not ending with <tt>00</tt> by using the query
     <tt>id=!&lt;00</tt>.
     (minor: api, webui)
  *  Remove support for <tt>//deprecated emphasized//</tt> Zettelmarkup.
     (minor: Zettelmarkup)
  *  Add options to profile the software. Profiling can be enabled at the
     command line or via the administrator console.
     (minor)
  *  Add computed zettel that lists all supported parser / recognized zettel
     syntaxes.
     (minor)
  *  Add API call to check for enabled authentication.
     (minor: api)
  *  Renewing an API access token works even if authentication is not enabled.
     This corresponds to the behaviour of optaining an access token.
     (minor: api)
  *  If there is nothing to return, use HTTP status code 204, instead of 200 +
     <tt>Content-Length: 0</tt>.
     (minor: api)
  *  Metadata key <tt>duplicates</tt> stores the duplicate file names, instead
     of just a boolean value that there were duplicate file names.
     (minor)
  *  Document autostarting Zettelstore on Windows, macOS, and Linux.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_1"></a><a id="0_1_0"></a>
<h2>Changes for Version 0.1 (2021-11-11)</h2>
  *  v0.1.3 (2021-12-15) fixes a bug where the modification date could be set
     when a new zettel is created.
  *  v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when
     more than one comparison is negated.
  *  v0.1.1 (2021-11-12) updates the documentation, mostly related to the
     deprecation of the <tt>//</tt> markup.
  *  Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup
     (emphasize, insert) is still allowed, but got a different syntax. The new
     syntax for <ins>inserted text</ins> is <tt>&gt;&gt;inserted&gt;&gt;</tt>,

     while its previous syntax now denotes <em>emphasized text</em>:
     <tt>__emphasized__</tt>. The previous syntax for emphasized text is now
     deprecated: <tt>//deprecated emphasized//</tt>. Starting with
     Version&nbsp;0.2.0, the deprecated syntax will not be supported. The
     reason is the collision with URLs that also contain the characters
     <tt>//</tt>. The ZMK encoding of a zettel may help with the transition
     (<tt>/v/{ZettelID}?_part=zettel&amp;_enc=zmk</tt>, on the Info page of
     each zettel in the WebUI). Additionally, all deprecated uses of
     <tt>//</tt> will be rendered with a dashed box within the WebUI.
     (breaking: Zettelmarkup).
  *  API client software is now a [https://zettelstore.de/client/|separate]
     project.
     (breaking)
  *  Initial support for HTTP security headers (Content-Security-Policy,
     Permissions-Policy, Referrer-Policy, X-Content-Type-Options,
     X-Frame-Options). Header values are currently some constant values.
     (possibly breaking: api, webui)
  *  Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup
     (strong, delete) is still allowed and replaces the visual elements
     syntactically. The visual appearance should not change (depends on your
     changes / additions to CSS zettel).
     (possibly breaking: Zettelmarkup).
  *  Add API endpoint <tt>POST /v</tt> to retrieve HTMl and text encoded
     strings from given ZettelMarkup encoded values. This will be used to
     render a HTML page from a given zettel: in many cases the title of
     a zettel must be treated separately.
     (minor: api)
  *  Add API endpoint <tt>/m</tt> to retrieve only the metadata of a zettel.

     (minor: api)
  *  New metadata value <tt>content-tags</tt> contains the tags that were given
     in the zettel content. To put it simply, <tt>all-tags</tt> = <tt>tags</tt>
     + <tt>content-tags</tt>.
     (minor)
  *  Calculating the context of a zettel stops at the home zettel.
     (minor: api, webui)
  *  When renaming or deleting a zettel, a warning will be given, if other
     zettel references the given zettel, or when &ldquo;deleting&rdquo; will
     uncover zettel in overlay box.
     (minor: webui)







|





|
|
|
|
|
|





|





|
|
|
>






|



|
|

|













|

|
|













|


|
>
|
|
|
|
|
|
|

|













|




|
>

|
|
|







647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
     (minor: directory and file/zip box)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_2"></a>
<h2>Changes for Version 0.2 (2022-01-19)</h2>
  *  v0.2.1 (2021-02-01) updates the license year in some documents
  *  Remove support for <code>;;small text;;</code> Zettelmarkup.
     (breaking: Zettelmarkup)
  *  On macOS, the downloadable executable program is now called
     &ldquo;zettelstore&rdquo;, as on all other Unix-like platforms.
     (possibly breaking: macOS)
  *  External metadata (e.g. for zettel with file extension other than
     <code>.zettel</code>) are stored in files without an extension. Metadata
     files with extension <code>.meta</code> are still recognized, but result
     in a warning message. In a future version (probably v0.3),
     <code>.meta</code> files will be treated as ordinary content files,
     possibly resulting in duplicate content. In other words: usage of
     <code>.meta</code> files for storing metadata is deprecated.
     (possibly breaking: directory and file box)
  *  Show unlinked references in info page of each zettel. Unlinked references
     are phrases within zettel content that might reference another zettel with
     the same title as the phase.
     (major: webui)
  *  Add endpoint <code>/u/{ID}</code> to retrieve unlinked references.
     (major: api)
  *  Provide a logging facility.
     Log messages are written to standard output. Messages with level
     &ldquo;information&rdquo; are also written to a circular buffer (of length
     8192) which can be retrieved via a computed zettel. There is a command
     line flag <code>-l LEVEL</code> to specify an application global logging
     level on startup (default: &ldquo;information&rdquo;). Logging level can
     also be changed via the administrator console, even for specific (sub-)
     services.
     (major)
  *  The internal handling of zettel files is rewritten. This allows less
     reloads ands detects when the directory containing the zettel files is
     removed. The API, WebUI, and the admin console allow to manually refresh
     the internal state on demand.
     (major: box, webui)
  *  <code>.zettel</code> files with YAML header are now correctly written.
     (bug)
  *  Selecting zettel based on their metadata allows the same syntax as
     searching for zettel content. For example, you can list all zettel that
     have an identifier not ending with <code>00</code> by using the query
     <code>id=!&lt;00</code>.
     (minor: api, webui)
  *  Remove support for <code>//deprecated emphasized//</code> Zettelmarkup.
     (minor: Zettelmarkup)
  *  Add options to profile the software. Profiling can be enabled at the
     command line or via the administrator console.
     (minor)
  *  Add computed zettel that lists all supported parser / recognized zettel
     syntaxes.
     (minor)
  *  Add API call to check for enabled authentication.
     (minor: api)
  *  Renewing an API access token works even if authentication is not enabled.
     This corresponds to the behaviour of optaining an access token.
     (minor: api)
  *  If there is nothing to return, use HTTP status code 204, instead of 200 +
     <code>Content-Length: 0</code>.
     (minor: api)
  *  Metadata key <code>duplicates</code> stores the duplicate file names,
     instead of just a boolean value that there were duplicate file names.
     (minor)
  *  Document autostarting Zettelstore on Windows, macOS, and Linux.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_1"></a><a id="0_1_0"></a>
<h2>Changes for Version 0.1 (2021-11-11)</h2>
  *  v0.1.3 (2021-12-15) fixes a bug where the modification date could be set
     when a new zettel is created.
  *  v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when
     more than one comparison is negated.
  *  v0.1.1 (2021-11-12) updates the documentation, mostly related to the
     deprecation of the <code>//</code> markup.
  *  Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup
     (emphasize, insert) is still allowed, but got a different syntax. The new
     syntax for <ins>inserted text</ins> is
     <code>&gt;&gt;inserted&gt;&gt;</code>, while its previous syntax now
     denotes <em>emphasized text</em>: <code>__emphasized__</code>. The
     previous syntax for emphasized text is now deprecated: <code>//deprecated
     emphasized//</code>. Starting with Version&nbsp;0.2.0, the deprecated
     syntax will not be supported. The reason is the collision with URLs that
     also contain the characters <code>//</code>. The ZMK encoding of a zettel
     may help with the transition
     (<code>/v/{ZettelID}?_part=zettel&amp;_enc=zmk</code>, on the Info page of
     each zettel in the WebUI). Additionally, all deprecated uses of
     <code>//</code> will be rendered with a dashed box within the WebUI.
     (breaking: Zettelmarkup).
  *  API client software is now a [https://zettelstore.de/client/|separate]
     project.
     (breaking)
  *  Initial support for HTTP security headers (Content-Security-Policy,
     Permissions-Policy, Referrer-Policy, X-Content-Type-Options,
     X-Frame-Options). Header values are currently some constant values.
     (possibly breaking: api, webui)
  *  Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup
     (strong, delete) is still allowed and replaces the visual elements
     syntactically. The visual appearance should not change (depends on your
     changes / additions to CSS zettel).
     (possibly breaking: Zettelmarkup).
  *  Add API endpoint <code>POST /v</code> to retrieve HTMl and text encoded
     strings from given ZettelMarkup encoded values. This will be used to
     render a HTML page from a given zettel: in many cases the title of
     a zettel must be treated separately.
     (minor: api)
  *  Add API endpoint <code>/m</code> to retrieve only the metadata of
     a zettel.
     (minor: api)
  *  New metadata value <code>content-tags</code> contains the tags that were
     given in the zettel content. To put it simply, <code>all-tags</code>
     = <code>tags</code> + <code>content-tags</code>.
     (minor)
  *  Calculating the context of a zettel stops at the home zettel.
     (minor: api, webui)
  *  When renaming or deleting a zettel, a warning will be given, if other
     zettel references the given zettel, or when &ldquo;deleting&rdquo; will
     uncover zettel in overlay box.
     (minor: webui)
679
680
681
682
683
684
685
686

687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755

756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806

807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882

883
884
885
886
887
888

889
890
891
892
893
894
895
896

897
898
899
900

901
902
903
904
905
906
907
908
909
910
911
912

913
914
915

916
917
918

919
920
921

922
923

924
925
926
927
928
929

930
931

932
933

934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989

990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051

1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
     (info)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_0_15"></a>
<h2>Changes for Version 0.0.15 (2021-09-17)</h2>
  *  Move again endpoint characters for authentication to make room for future
     features. WebUI authentication moves from <tt>/a</tt> to <tt>/i</tt>

     (login) and <tt>/i?logout</tt> (logout). API authentication moves from
     <tt>/v</tt> to </tt>/a</tt>. JSON-based basic zettel handling moves from
     <tt>/z</tt> to <tt>/j</tt> and <tt>/z/{ID}</tt> to <tt>/j/{ID}</tt>. Since
     the API client is updated too, this should not be a breaking change for
     most users.
     (minor: api, webui; possibly breaking)
  *  Add API endpoint <tt>/v/{ID}</tt> to retrieve an evaluated zettel in
     various encodings. Mostly replaces endpoint <tt>/z/{ID}</tt> for other
     encodings except &ldquo;json&rdquo; and &ldquo;raw&rdquo;. Endpoint
     <tt>/j/{ID}</tt> now only returns JSON data, endpoint <tt>/z/{ID}</tt> is
     used to retrieve plain zettel data (previously called &ldquo;raw&rdquo;).
     See documentation for details.
     (major: api; breaking)
  *  Metadata values of type <em>tag set</em> (the metadata with key
     <tt>tags</tt> is its most prominent example), are now compared in
     a case-insensitive manner. Tags that only differ in upper / lower case
     character are now treated identical. This might break your workflow, if
     you depend on case-sensitive comparison of tag values. Tag values are
     translated to their lower case equivalent before comparing them and when
     you edit a zettel through Zettelstore. If you just modify the zettel
     files, your tag values remain unchanged.
     (major; breaking)
  *  Endpoint <tt>/z/{ID}</tt> allows the same methods as endpoint
     <tt>/j/{ID}</tt>: <tt>GET</tt> retrieves zettel (see above), <tt>PUT</tt>
     updates a zettel, <tt>DELETE</tt> deletes a zettel, <tt>MOVE</tt> renames
     a zettel. In addtion, <tt>POST /z</tt> will create a new zettel. When
     zettel data must be given, the format is plain text, with metadata
     separated from content by an empty line. See documentation for more
     details.
     (major: api (plus WebUI for some details))
  *  Allows to transclude / expand the content of another zettel into a target
     zettel when the zettel is rendered. By using the syntax of embedding an
     image (which is some kind of expansion too), the first top-level paragraph
     of a zettel may be transcluded into the target zettel. Endless recursion
     is checked, as well as a possible &ldquo;transclusion bomb &rdquo;
     (similar to a XML bomb). See manual for details.
     (major: zettelmarkup)
  *  The endpoint <tt>/z</tt> allows to list zettel in a simpler format than
     endpoint <tt>/j</tt>: one line per zettel, and only zettel identifier plus
     zettel title.
     (minor: api)
  *  Folgezettel are now displayed with full title at the bottom of a page.
     (minor: webui)
  *  Add API endpoint <tt>/p/{ID}</tt> to retrieve a parsed, but not evaluated
     zettel in various encodings.
     (minor: api)
  *  Fix: do not list a shadowed zettel that matches the select criteria.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_0_14"></a>
<h2>Changes for Version 0.0.14 (2021-07-23)</h2>
  *  Rename &ldquo;place&rdquo; into &ldquo;box&rdquo;. This also affects the
     configuration keys to specify boxes <tt>box-uri<em>X</em></tt> (previously
     <tt>place-uri-<em>X</em></tt>. Older changes documented here are renamed
     too.
     (breaking)
  *  Add API for creating, updating, renaming, and deleting zettel.
     (major: api)
  *  Initial API client for Go.
     (major: api)
  *  Remove support for paging of WebUI list. Runtime configuration key
     <tt>list-page-size</tt> is removed. If you still specify it, it will be
     ignored.
     (major: webui)
  *  Use endpoint <tt>/v</tt> for user authentication via API. Endpoint
     <tt>/a</tt> is now used for the web user interface only. Similar, endpoint
     <tt>/y</tt> (&ldquo;zettel context&rdquo;) is renamed to <tt>/x</tt>.

     (minor, possibly breaking)
  *  Type of used-defined metadata is determined by suffix of key:
     <tt>-number</tt>, <tt>-url</tt>, <tt>-zid</tt> will result the values to
     be interpreted as a number, an URL, or a zettel identifier.
     (minor, but possibly breaking if you already used a metadata key with
     above suffixes, but as a string type)
  *  New <tt>user-role</tt> &ldquo;creator&rdquo;, which is only allowed to
     create new zettel (except user zettel). This role may only read and update
     public zettel or its own user zettel. Added to support future client
     software (e.g. on a mobile device) that automatically creates new zettel
     but, in case of a password loss, should not allow to read existing zettel.
     (minor, possibly breaking, because new zettel template zettel must always
     prepend the string <tt>new-</tt> before metdata keys that should be
     transferred to the new zettel)
  *  New suported metadata key <tt>box-number</tt>, which gives an indication
     from which box the zettel was loaded.
     (minor)
  *  New supported syntax <tt>html</tt>.
     (minor)
  *  New predefined zettel &ldquo;User CSS&rdquo; that can be used to redefine
     some predefined CSS (without modifying the base CSS zettel).
     (minor: webui)
  *  When a user moves a zettel file with additional characters into the box
     directory, these characters are preserved when zettel is updated.
     (bug)
  *  The phase &ldquo;filtering a zettel list&rdquo; is more precise
     &ldquo;selecting zettel&rdquo;
     (documentation)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_0_13"></a>
<h2>Changes for Version 0.0.13 (2021-06-01)</h2>
  *  Startup configuration <tt>box-<em>X</em>-uri</tt> (where <em>X</em> is a
     number greater than zero) has been renamed to
     <tt>box-uri-<em>X</em></tt>.
     (breaking)
  *  Web server processes startup configuration <tt>url-prefix</tt>. There is
     no need for stripping the prefix by a front-end web server any more.
     (breaking: webui, api)
  *  Administrator console (only optional accessible locally). Enable it only
     on systems with a single user or with trusted users. It is disabled by
     default.
     (major: core)
  *  Remove visibility value &ldquo;simple-expert&rdquo; introduced in
     [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There
     was a name collision with the &ldquo;simple&rdquo; directory box sub-type.
     (major)
  *  For security reasons, HTML blocks are not encoded as HTML if they contain
     certain snippets, such as <tt>&lt;script</tt> or <tt>&lt;iframe</tt>.
     These may be caused by using CommonMark as a zettel syntax.

     (major)
  *  Full-text search can be a prefix search or a search for equal words, in
     addition to the search whether a word just contains word of the search
     term.
     (minor: api, webui)
  *  Full-text search for URLs, with above additional operators.
     (minor: api, webui)
  *  Add system zettel about license, contributors, and dependencies (and their
     license).
     For a nicer layout of zettel identifier, the zettel about environment
     values and about runtime metrics got new zettel identifier. This affects
     only user that referenced those zettel.
     (minor)
  *  Local images that cannot be read (not found or no access rights) are
     substituted with the new default image, a spinning emoji.
     See [/file?name=box/constbox/emoji_spin.gif].
     (minor: webui)
  *  Add zettelmarkup syntax for a table row that should be ignored:
     <tt>|%</tt>. This allows to paste output of the administrator console into
     a zettel.
     (minor: zmk)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_0_12"></a>
<h2>Changes for Version 0.0.12 (2021-04-16)</h2>
  *  Raise the per-process limit of open files on macOS to 1.048.576. This
     allows most macOS users to use at least 500.000 zettel. That should be
     enough for the near future.
     (major)
  *  Mitigate the shortcomings of the macOS version by introducing types of
     directory boxes. The original directory box type is now called "notify"
     (the default value). There is a new type called "simple". This new type
     does not notify Zettelstore when some of the underlying Zettel files
     change.
     (major)
  *  Add new startup configuration <tt>default-dir-box-type</tt>, which gives
     the default value for specifying a directory box type. The default value
     is &ldquo;notify&rdquo;. On macOS, the default value may be changed
     &ldquo;simple&rdquo; if some errors occur while raising the per-process
     limit of open files.
     (minor)

<a id="0_0_11"></a>
<h2>Changes for Version 0.0.11 (2021-04-05)</h2>
  *  New box schema "file" allows to read zettel from a ZIP file.
     A zettel collection can now be packaged and distributed easier.
     (major: server)
  *  Non-restricted search is a full-text search. The search string will be
     normalized according to Unicode NFKD. Every character that is not a letter
     or a number will be ignored for the search. It is sufficient if the words
     to be searched are part of words inside a zettel, both content and
     metadata.
     (major: api, webui)
  *  A zettel can be excluded from being indexed (and excluded from being found
     in a search) if it contains the metadata <tt>no-index: true</tt>.
     (minor: api, webui)
  *  Menu bar is shown when displaying error messages.
     (minor: webui)
  *  When selecting zettel, it can be specified that a given value should
     <em>not</em> match. Previously, only the whole select criteria could be
     negated (which is still possible).
     (minor: api, webui)
  *  You can select a zettel by specifying that specific metadata keys must
     (or must not) be present.
     (minor: api, webui)
  *  Context of a zettel (introduced in version 0.0.10) does not take tags into
     account any more. Using some tags for determining the context resulted
     into erratic, non-deterministic context lists.
     (minor: api, webui)
  *  Selecting zettel depending on tag values can be both by comparing only the
     prefix or the whole string. If a search value begins with '#', only zettel
     with the exact tag will be returned. Otherwise a zettel will be returned
     if the search string just matches the prefix of only one of its tags.
     (minor: api, webui)
  *  Many smaller bug fixes and improvements, to the software and to the documentation.


A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
  #  You update the per-process limit of open files on macOS.
  #  You setup a virtualization environment to run Zettelstore on Linux or Windows.

  #  You wait for version 0.0.12 which addresses this issue.

<a id="0_0_10"></a>
<h2>Changes for Version 0.0.10 (2021-02-26)</h2>
  *  Menu item &ldquo;Home&rdquo; now redirects to a home zettel.
     Its default identifier is <tt>000100000000</tt>.
     The identifier can be changed with configuration key <tt>home-zettel</tt>, which supersedes key <tt>start</tt>.
     The default home zettel contains some welcoming information for the new user.

     (major: webui)
  *  Show context of a zettel by following all backward and/or forward reference
     up to a defined depth and list the resulting zettel. Additionally, some zettel
     with similar tags as the initial zettel are also taken into account.

     (major: api, webui)
  *  A zettel that references other zettel within first-level list items, can act
     as a &ldquo;table of contents&rdquo; zettel.
     The API endpoint <tt>/o/{ID}</tt> allows to retrieve the referenced zettel in
     the same order as they occur in the zettel.
     (major: api)
  *  The zettel &ldquo;New Menu&rdquo; with identifier <tt>00000000090000</tt> contains
     a list of all zettel that should act as a template for new zettel.
     They are listed in the WebUIs &rdquo;New&ldquo; menu.
     This is an application of the previous item.
     It supersedes the usage of a role <tt>new-template</tt> introduced in [#0_0_6|version 0.0.6].
     <b>Please update your zettel if you make use of the now deprecated feature.</b>

     (major: webui)
  *  A reference that starts with two slash characters (&ldquo;<code>//</code>&rdquo;)
     it will be interpreted relative to the value of <code>url-prefix</code>.

     For example, if <code>url-prefix</code> has the value <code>/manual/</code>,
     the reference <code>&lbrack;&lbrack;Zettel list|//h]]</code> will render as
     <code>&lt;a href="/manual/h">Zettel list&lt;/a></code>. (minor: syntax)

  *  Searching/selecting ignores the leading '#' character of tags.
     (minor: api, webui)
  *  When result of selecting or searching is presented, the query is written as the page heading.

     (minor: webui)
  *  A reference to a zettel that contains a URL fragment, will now be processed by the indexer.

     (bug: server)
  *  Runtime configuration key <tt>marker-external</tt> now defaults to
     &ldquo;&amp;#10138;&rdquo; (&ldquo;&#10138;&rdquo;). It is more beautiful
     than the previous &ldquo;&amp;#8599;&amp;#xfe0e;&rdquo;
     (&ldquo;&#8599;&#65038;&rdquo;), which also needed the additional
     &ldquo;&amp;#xfe0e;&rdquo; to disable the conversion to an emoji on iPadOS.

     (minor: webui)
  *  A pre-build binary for macOS ARM64 (also known as Apple silicon) is available.

     (minor: infrastructure)
  *  Many smaller bug fixes and improvements, to the software and to the documentation.


<a id="0_0_9"></a>
<h2>Changes for Version 0.0.9 (2021-01-29)</h2>
This is the first version that is managed by [https://fossil-scm.org|Fossil]
instead of GitHub. To access older versions, use the Git repository under
[https://github.com/zettelstore/zettelstore-github|zettelstore-github].

<h3>Server / API</h3>
  *  (major) Support for property metadata.
             Metadata key <tt>published</tt> is the first example of such
             a property.
  *  (major) A background activity (called <i>indexer</i>) continuously
             monitors zettel changes to establish the reverse direction of
             found internal links. This affects the new metadata keys
             <tt>precursor</tt> and <tt>folge</tt>. A user specifies the
             precursor of a zettel and the indexer computes the property
             metadata for
             [https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel].
             Metadata keys with type &ldquo;Identifier&rdquo; or
             &ldquo;IdentifierSet&rdquo; that have no inverse key (like
             <tt>precursor</tt> and <tt>folge</tt> with add to the key
             <tt>forward</tt> that also collects all internal links within the
             content. The computed inverse is <tt>backward</tt>, which provides
             all backlinks. The key <tt>back</tt> is computed as the value of
             <tt>backward</tt>, but without forward links. Therefore,
             <tt>back</tt> is something like the list of &ldquo;smart
             backlinks&rdquo;.
  *  (minor) If Zettelstore is being stopped, an appropriate message is written
             in the console log.
  *  (minor) New computed zettel with environmental data, the list of supported
             meta data keys, and statistics about all configured zettel boxes.
             Some other computed zettel got a new identifier (to make room for
             other variant).
  *  (minor) Remove zettel <tt>00000000000004</tt>, which contained the Go
             version that produced the Zettelstore executable. It was too
             specific to the current implementation. This information is now
             included in zettel <tt>00000000000006</tt> (<i>Zettelstore
             Environment Values</i>).
  *  (minor) Predefined templates for new zettel do not contain any value for
             attribute <tt>visibility</tt> any more.
  *  (minor) Add a new metadata key type called &ldquo;Zettelmarkup&rdquo;.
             It is a non-empty string, that will be formatted with
             Zettelmarkup. <tt>title</tt> and <tt>default-title</tt> have this
             type.
  *  (major) Rename zettel syntax &ldquo;meta&rdquo; to &ldquo;none&rdquo;.
             Please update the <i>Zettelstore Runtime Configuration</i> and all
             other zettel that previously used the value &ldquo;meta&rdquo;.
             Other zettel are typically user zettel, used for authentication.
             However, there is no real harm, if you do not update