Zettelstore

Check-in Differences
Login

Check-in Differences

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

Difference From trunk To v0.19.0

2025-02-16
18:41
Fix some issues with go.sum ... (Leaf check-in: 07ab548853 user: t73fde tags: trunk)
18:09
Make use of Sx list iters ... (check-in: 7ac84dda26 user: t73fde tags: trunk)
2024-12-13
14:01
Increase version to 0.20.0-dev to begin next development cycle ... (check-in: 8d481c1e92 user: stern tags: trunk)
13:26
Version 0.19.0 ... (check-in: bb751329ab user: stern tags: trunk, release, v0.19.0)
12:36
Update to latest zettelstore client ... (check-in: 5ed6d8e8e3 user: stern tags: trunk)

Changes to VERSION.

1


1
-
+
0.20.0-dev
0.19.0

Changes to ast/ast.go.

13
14
15
16
17
18
19
20
21
22



23
24
25
26
27
28
29
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27
28
29







-
-
-
+
+
+








// Package ast provides the abstract syntax tree for parsed zettel content.
package ast

import (
	"net/url"

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

// ZettelNode is the root node of the abstract syntax tree.
// It is *not* part of the visitor pattern.
type ZettelNode struct {
	Meta    *meta.Meta     // Original metadata
	Content zettel.Content // Original content

Changes to ast/block.go.

76
77
78
79
80
81
82
83

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

83
84
85
86
87
88
89
90







-
+







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

// Constants for VerbatimCode
const (
	_               VerbatimKind = iota
	VerbatimZettel               // Zettel content
	VerbatimCode                 // Program code
	VerbatimProg                 // Program code
	VerbatimEval                 // Code to be externally interpreted. Syntax is stored in default attribute.
	VerbatimComment              // Block comment
	VerbatimHTML                 // Block HTML, e.g. for Markdown
	VerbatimMath                 // Block math mode
)

func (*VerbatimNode) blockNode() { /* Just a marker */ }
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
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







-
+











-
-
-
-
-








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

// WalkChildren walks down to the cells.
func (tn *TableNode) WalkChildren(v Visitor) {
	if header := tn.Header; header != nil {
		for i := range header {
			Walk(v, header[i]) // Otherwise changes will not go back
			Walk(v, &header[i].Inlines) // Otherwise changes will not go back
		}
	}
	if rows := tn.Rows; rows != nil {
		for _, row := range rows {
			for i := range row {
				Walk(v, &row[i].Inlines) // Otherwise changes will not go back
			}
		}
	}
}

// WalkChildren walks the list of inline elements.
func (cell *TableCell) WalkChildren(v Visitor) {
	Walk(v, &cell.Inlines) // Otherwise changes will not go back
}

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

// TranscludeNode specifies block content from other zettel to embedded in
// current zettel
type TranscludeNode struct {
	Attrs attrs.Attributes
	Ref   *Reference

Changes to ast/inline.go.

22
23
24
25
26
27
28
29
30
31


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



29
30

31
32
33
34
35
36
37







-
-
-
+
+
-







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

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

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

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

// TextNode just contains some text.
type TextNode struct {
195
196
197
198
199
200
201

202

203
204
205

206
207
208
209
210
211
212
193
194
195
196
197
198
199
200

201
202
203
204
205
206
207
208
209
210
211
212







+
-
+



+








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

// Constants for LiteralCode
const (
	_              LiteralKind = iota
	LiteralZettel              // Zettel content
	LiteralCode                // Inline program code
	LiteralProg                // Inline program code
	LiteralInput               // Computer input, e.g. Keyboard strokes
	LiteralOutput              // Computer output
	LiteralComment             // Inline comment
	LiteralHTML                // Inline HTML, e.g. for Markdown
	LiteralMath                // Inline math mode
)

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

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

Changes to ast/ref.go.

14
15
16
17
18
19
20
21

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

21
22
23
24
25
26
27
28







-
+







package ast

import (
	"net/url"
	"strings"

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

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

// ParseReference parses a string and returns a reference.
func ParseReference(s string) *Reference {

Deleted ast/sztrans/sztrans.go.

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





















































































































































































































































































































































































































































































































































































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

// Package sztrans allows to transform a sz representation of text into an
// abstract syntax tree.
package sztrans

import (
	"fmt"
	"log"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/sz"
	"zettelstore.de/z/ast"
)

type transformer struct{}

// GetBlockSlice returns the sz representations as a AST BlockSlice
func GetBlockSlice(pair *sx.Pair) (ast.BlockSlice, error) {
	if pair == nil {
		return nil, nil
	}
	var t transformer
	if obj := sz.Walk(&t, pair, nil); !obj.IsNil() {
		if sxn, isNode := obj.(sxNode); isNode {
			if bs, ok := sxn.node.(*ast.BlockSlice); ok {
				return *bs, nil
			}
			return nil, fmt.Errorf("no BlockSlice AST: %T/%v for %v", sxn.node, sxn.node, pair)
		}
		return nil, fmt.Errorf("no AST for %v: %v", pair, obj)
	}
	return nil, fmt.Errorf("error walking %v", pair)
}

func (t *transformer) VisitBefore(pair *sx.Pair, _ *sx.Pair) (sx.Object, bool) {
	if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol {
		switch sym {
		case sz.SymText:
			if p := pair.Tail(); p != nil {
				if s, isString := sx.GetString(p.Car()); isString {
					return sxNode{&ast.TextNode{Text: s.GetValue()}}, true
				}
			}
		case sz.SymSoft:
			return sxNode{&ast.BreakNode{Hard: false}}, true
		case sz.SymHard:
			return sxNode{&ast.BreakNode{Hard: true}}, true
		case sz.SymLiteralCode:
			return handleLiteral(ast.LiteralCode, pair.Tail())
		case sz.SymLiteralComment:
			return handleLiteral(ast.LiteralComment, pair.Tail())
		case sz.SymLiteralInput:
			return handleLiteral(ast.LiteralInput, pair.Tail())
		case sz.SymLiteralMath:
			return handleLiteral(ast.LiteralMath, pair.Tail())
		case sz.SymLiteralOutput:
			return handleLiteral(ast.LiteralOutput, pair.Tail())
		case sz.SymThematic:
			return sxNode{&ast.HRuleNode{Attrs: sz.GetAttributes(pair.Tail().Head())}}, true
		case sz.SymVerbatimComment:
			return handleVerbatim(ast.VerbatimComment, pair.Tail())
		case sz.SymVerbatimEval:
			return handleVerbatim(ast.VerbatimEval, pair.Tail())
		case sz.SymVerbatimHTML:
			return handleVerbatim(ast.VerbatimHTML, pair.Tail())
		case sz.SymVerbatimMath:
			return handleVerbatim(ast.VerbatimMath, pair.Tail())
		case sz.SymVerbatimCode:
			return handleVerbatim(ast.VerbatimCode, pair.Tail())
		case sz.SymVerbatimZettel:
			return handleVerbatim(ast.VerbatimZettel, pair.Tail())
		case sz.SymTransclude:
			return handleTransclude(pair.Tail())
		}
	}
	return sx.Nil(), false
}

func handleLiteral(kind ast.LiteralKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if s, isString := sx.GetString(curr.Car()); isString {
				return sxNode{&ast.LiteralNode{
					Kind:    kind,
					Attrs:   attrs,
					Content: []byte(s.GetValue())}}, true
			}
		}
	}
	return nil, false
}

func handleVerbatim(kind ast.VerbatimKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if s, isString := sx.GetString(curr.Car()); isString {
				return sxNode{&ast.VerbatimNode{
					Kind:    kind,
					Attrs:   attrs,
					Content: []byte(s.GetValue()),
				}}, true
			}
		}
	}
	return nil, false
}

func handleTransclude(rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			ref := collectReference(curr.Head())
			return sxNode{&ast.TranscludeNode{
				Attrs: attrs,
				Ref:   ref,
			}}, true
		}
	}
	return nil, false
}

func (t *transformer) VisitAfter(pair *sx.Pair, _ *sx.Pair) (sx.Object, bool) {
	if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol {
		switch sym {
		case sz.SymBlock:
			bns := collectBlocks(pair.Tail())
			return sxNode{&bns}, true
		case sz.SymPara:
			return sxNode{&ast.ParaNode{Inlines: collectInlines(pair.Tail())}}, true
		case sz.SymHeading:
			return handleHeading(pair.Tail())
		case sz.SymListOrdered:
			return sxNode{&ast.NestedListNode{
				Kind:  ast.NestedListOrdered,
				Items: collectItemSlices(pair.Tail()),
				Attrs: nil}}, true
		case sz.SymListUnordered:
			return sxNode{&ast.NestedListNode{
				Kind:  ast.NestedListUnordered,
				Items: collectItemSlices(pair.Tail()),
				Attrs: nil}}, true
		case sz.SymListQuote:
			return sxNode{&ast.NestedListNode{
				Kind:  ast.NestedListQuote,
				Items: collectItemSlices(pair.Tail()),
				Attrs: nil}}, true
		case sz.SymDescription:
			return handleDescription(pair.Tail())
		case sz.SymTable:
			return handleTable(pair.Tail())
		case sz.SymCell:
			return handleCell(ast.AlignDefault, pair.Tail())
		case sz.SymCellCenter:
			return handleCell(ast.AlignCenter, pair.Tail())
		case sz.SymCellLeft:
			return handleCell(ast.AlignLeft, pair.Tail())
		case sz.SymCellRight:
			return handleCell(ast.AlignRight, pair.Tail())
		case sz.SymRegionBlock:
			return handleRegion(ast.RegionSpan, pair.Tail())
		case sz.SymRegionQuote:
			return handleRegion(ast.RegionQuote, pair.Tail())
		case sz.SymRegionVerse:
			return handleRegion(ast.RegionVerse, pair.Tail())

		case sz.SymLinkHosted:
			return handleLink(ast.RefStateHosted, pair.Tail())
		case sz.SymLinkInvalid:
			return handleLink(ast.RefStateInvalid, pair.Tail())
		case sz.SymLinkZettel:
			return handleLink(ast.RefStateZettel, pair.Tail())
		case sz.SymLinkSelf:
			return handleLink(ast.RefStateSelf, pair.Tail())
		case sz.SymLinkFound:
			return handleLink(ast.RefStateFound, pair.Tail())
		case sz.SymLinkBroken:
			return handleLink(ast.RefStateBroken, pair.Tail())
		case sz.SymLinkHosted:
			return handleLink(ast.RefStateHosted, pair.Tail())
		case sz.SymLinkBased:
			return handleLink(ast.RefStateBased, pair.Tail())
		case sz.SymLinkQuery:
			return handleLink(ast.RefStateQuery, pair.Tail())
		case sz.SymLinkExternal:
			return handleLink(ast.RefStateExternal, pair.Tail())
		case sz.SymEmbed:
			return handleEmbed(pair.Tail())
		case sz.SymCite:
			return handleCite(pair.Tail())
		case sz.SymMark:
			return handleMark(pair.Tail())
		case sz.SymEndnote:
			return handleEndnote(pair.Tail())
		case sz.SymFormatDelete:
			return handleFormat(ast.FormatDelete, pair.Tail())
		case sz.SymFormatEmph:
			return handleFormat(ast.FormatEmph, pair.Tail())
		case sz.SymFormatInsert:
			return handleFormat(ast.FormatInsert, pair.Tail())
		case sz.SymFormatMark:
			return handleFormat(ast.FormatMark, pair.Tail())
		case sz.SymFormatQuote:
			return handleFormat(ast.FormatQuote, pair.Tail())
		case sz.SymFormatSpan:
			return handleFormat(ast.FormatSpan, pair.Tail())
		case sz.SymFormatSub:
			return handleFormat(ast.FormatSub, pair.Tail())
		case sz.SymFormatSuper:
			return handleFormat(ast.FormatSuper, pair.Tail())
		case sz.SymFormatStrong:
			return handleFormat(ast.FormatStrong, pair.Tail())
		}
		log.Println("MISS", pair)
	}
	return sxNode{}, false
}

func collectBlocks(lst *sx.Pair) (result ast.BlockSlice) {
	for val := range lst.Values() {
		if sxn, isNode := val.(sxNode); isNode {
			if bn, isInline := sxn.node.(ast.BlockNode); isInline {
				result = append(result, bn)
			}
		}
	}
	return result
}

func collectInlines(lst *sx.Pair) (result ast.InlineSlice) {
	for val := range lst.Values() {
		if sxn, isNode := val.(sxNode); isNode {
			if in, isInline := sxn.node.(ast.InlineNode); isInline {
				result = append(result, in)
			}
		}
	}
	return result
}

func handleHeading(rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		if num, isNumber := rest.Car().(sx.Int64); isNumber && num > 0 && num < 6 {
			if curr := rest.Tail(); curr != nil {
				attrs := sz.GetAttributes(curr.Head())
				if curr = curr.Tail(); curr != nil {
					if sSlug, isSlug := sx.GetString(curr.Car()); isSlug {
						if curr = curr.Tail(); curr != nil {
							if sUniq, isUniq := sx.GetString(curr.Car()); isUniq {
								return sxNode{&ast.HeadingNode{
									Level:    int(num),
									Attrs:    attrs,
									Slug:     sSlug.GetValue(),
									Fragment: sUniq.GetValue(),
									Inlines:  collectInlines(curr.Tail()),
								}}, true
							}
						}
					}
				}
			}
		}
	}
	log.Println("HEAD", rest)
	return nil, false
}

func collectItemSlices(lst *sx.Pair) (result []ast.ItemSlice) {
	for val := range lst.Values() {
		if sxn, isNode := val.(sxNode); isNode {
			if bns, isBlockSlice := sxn.node.(*ast.BlockSlice); isBlockSlice {
				items := make(ast.ItemSlice, len(*bns))
				for i, bn := range *bns {
					if it, ok := bn.(ast.ItemNode); ok {
						items[i] = it
					}
				}
				result = append(result, items)
			}
			if ins, isInline := sxn.node.(*ast.InlineSlice); isInline {
				items := make(ast.ItemSlice, len(*ins))
				for i, bn := range *ins {
					if it, ok := bn.(ast.ItemNode); ok {
						items[i] = it
					}
				}
				result = append(result, items)
			}
		}
	}
	return result
}

func handleDescription(rest *sx.Pair) (sx.Object, bool) {
	var descs []ast.Description
	for curr := rest; curr != nil; {
		term := collectInlines(curr.Head())
		curr = curr.Tail()
		if curr == nil {
			descr := ast.Description{Term: term, Descriptions: nil}
			descs = append(descs, descr)
			break
		}

		sxn, isNode := curr.Car().(sxNode)
		if !isNode {
			descs = nil
			break
		}
		blocks, isBlocks := sxn.node.(*ast.BlockSlice)
		if !isBlocks {
			descs = nil
			break
		}

		descSlice := make([]ast.DescriptionSlice, 0, len(*blocks))
		for _, bn := range *blocks {
			bns, isBns := bn.(*ast.BlockSlice)
			if !isBns {
				continue
			}
			ds := make(ast.DescriptionSlice, 0, len(*bns))
			for _, b := range *bns {
				if defNode, isDef := b.(ast.DescriptionNode); isDef {
					ds = append(ds, defNode)
				}
			}
			descSlice = append(descSlice, ds)
		}

		descr := ast.Description{Term: term, Descriptions: descSlice}
		descs = append(descs, descr)

		curr = curr.Tail()
	}
	if len(descs) > 0 {
		return sxNode{&ast.DescriptionListNode{Descriptions: descs}}, true
	}
	log.Println("DESC", rest)
	return nil, false
}

func handleTable(rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		header := collectRow(rest.Head())
		cols := len(header)

		var rows []ast.TableRow
		for curr := range rest.Tail().Pairs() {
			row := collectRow(curr.Head())
			rows = append(rows, row)
			cols = max(cols, len(row))
		}
		align := make([]ast.Alignment, cols)
		for i := range cols {
			align[i] = ast.AlignDefault
		}

		return sxNode{&ast.TableNode{
			Header: header,
			Align:  align,
			Rows:   rows,
		}}, true
	}
	log.Println("TABL", rest)
	return nil, false
}

func collectRow(lst *sx.Pair) (row ast.TableRow) {
	for curr := range lst.Values() {
		if sxn, isNode := curr.(sxNode); isNode {
			if cell, isCell := sxn.node.(*ast.TableCell); isCell {
				row = append(row, cell)
			}
		}
	}
	return row
}

func handleCell(align ast.Alignment, rest *sx.Pair) (sx.Object, bool) {
	return sxNode{&ast.TableCell{
		Align:   align,
		Inlines: collectInlines(rest),
	}}, true
}

func handleRegion(kind ast.RegionKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if blockList := curr.Head(); blockList != nil {
				return sxNode{&ast.RegionNode{
					Kind:    kind,
					Attrs:   attrs,
					Blocks:  collectBlocks(blockList),
					Inlines: collectInlines(curr.Tail()),
				}}, true
			}
		}
	}
	log.Println("REGI", rest)
	return nil, false
}

func handleLink(state ast.RefState, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if sref, isString := sx.GetString(curr.Car()); isString {
				ref := ast.ParseReference(sref.GetValue())
				ref.State = state
				ins := collectInlines(curr.Tail())
				return sxNode{&ast.LinkNode{
					Attrs:   attrs,
					Ref:     ref,
					Inlines: ins,
				}}, true
			}
		}
	}
	log.Println("LINK", state, rest)
	return nil, false
}

func handleEmbed(rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if ref := collectReference(curr.Head()); ref != nil {
				return sxNode{&ast.EmbedRefNode{
					Attrs:   attrs,
					Ref:     ref,
					Syntax:  "",
					Inlines: collectInlines(curr.Tail()),
				}}, true
			}
		}
	}
	log.Println("EMBE", rest)
	return nil, false
}

func collectReference(pair *sx.Pair) *ast.Reference {
	if pair != nil {
		if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol {
			if next := pair.Tail(); next != nil {
				if sRef, isString := sx.GetString(next.Car()); isString {
					ref := ast.ParseReference(sRef.GetValue())
					switch sym {
					case sz.SymRefStateInvalid:
						ref.State = ast.RefStateInvalid
					case sz.SymRefStateZettel:
						ref.State = ast.RefStateZettel
					case sz.SymRefStateSelf:
						ref.State = ast.RefStateSelf
					case sz.SymRefStateFound:
						ref.State = ast.RefStateFound
					case sz.SymRefStateBroken:
						ref.State = ast.RefStateBroken
					case sz.SymRefStateHosted:
						ref.State = ast.RefStateHosted
					case sz.SymRefStateBased:
						ref.State = ast.RefStateBased
					case sz.SymRefStateQuery:
						ref.State = ast.RefStateQuery
					case sz.SymRefStateExternal:
						ref.State = ast.RefStateExternal
					}
					return ref
				}
			}
		}
	}
	return nil
}

func handleCite(rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if sKey, isString := sx.GetString(curr.Car()); isString {
				return sxNode{&ast.CiteNode{
					Attrs:   attrs,
					Key:     sKey.GetValue(),
					Inlines: collectInlines(curr.Tail()),
				}}, true
			}
		}
	}
	log.Println("CITE", rest)
	return nil, false
}

func handleMark(rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		if sMark, isMarkS := sx.GetString(rest.Car()); isMarkS {
			if curr := rest.Tail(); curr != nil {
				if sSlug, isSlug := sx.GetString(curr.Car()); isSlug {
					if curr = curr.Tail(); curr != nil {
						if sUniq, isUniq := sx.GetString(curr.Car()); isUniq {
							return sxNode{&ast.MarkNode{
								Mark:     sMark.GetValue(),
								Slug:     sSlug.GetValue(),
								Fragment: sUniq.GetValue(),
								Inlines:  collectInlines(curr.Tail()),
							}}, true
						}
					}
				}
			}
		}
	}
	log.Println("MARK", rest)
	return nil, false
}

func handleEndnote(rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		return sxNode{&ast.FootnoteNode{
			Attrs:   attrs,
			Inlines: collectInlines(rest.Tail()),
		}}, true
	}
	log.Println("ENDN", rest)
	return nil, false
}

func handleFormat(kind ast.FormatKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := sz.GetAttributes(rest.Head())
		return sxNode{&ast.FormatNode{
			Kind:    kind,
			Attrs:   attrs,
			Inlines: collectInlines(rest.Tail()),
		}}, true
	}
	log.Println("FORM", kind, rest)
	return nil, false
}

type sxNode struct {
	node ast.Node
}

func (sxNode) IsNil() bool        { return false }
func (sxNode) IsAtom() bool       { return true }
func (n sxNode) String() string   { return fmt.Sprintf("%T/%v", n.node, n.node) }
func (n sxNode) GoString() string { return n.String() }
func (n sxNode) IsEqual(other sx.Object) bool {
	return n.String() == other.String()
}

Changes to auth/auth.go.

13
14
15
16
17
18
19
20
21
22
23




24
25
26
27
28
29
30
13
14
15
16
17
18
19




20
21
22
23
24
25
26
27
28
29
30







-
-
-
-
+
+
+
+








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

import (
	"time"

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

// BaseManager allows to check some base auth modes.
type BaseManager interface {
	// IsReadonly returns true, if the systems is configured to run in read-only-mode.
	IsReadonly() bool
}

Changes to auth/cred/cred.go.

14
15
16
17
18
19
20
21

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

21

22
23
24
25
26
27
28







-
+
-







// Package cred provides some function for handling credentials.
package cred

import (
	"bytes"

	"golang.org/x/crypto/bcrypt"

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

// HashCredential returns a hashed vesion of the given credential
func HashCredential(zid id.Zid, ident, credential string) (string, error) {
	fullCredential := createFullCredential(zid, ident, credential)
	res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost)
	if err != nil {

Changes to auth/impl/impl.go.

17
18
19
20
21
22
23
24

25
26
27
28
29
30
31


32
33
34
35
36
37
38
17
18
19
20
21
22
23

24

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







-
+
-






+
+







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

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

type myAuth struct {
	readonly bool
	owner    id.Zid
	secret   []byte
}
79
80
81
82
83
84
85
86

87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
80
81
82
83
84
85
86

87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102







-
+







-
+







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

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

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

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

162
163
164
165
166
167
168
169
170


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


170
171
172
173
174
175
176
177
178
179
180







-
-
+
+









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

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

Changes to auth/policy/anon.go.

10
11
12
13
14
15
16
17
18
19

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

17
18
19
20
21
22
23
24
25
26







-


+







// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

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

type anonPolicy struct {
	authConfig config.AuthConfig
	pre        auth.Policy
}

Changes to auth/policy/box.go.

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


28
29
30
31
32
33
34
12
13
14
15
16
17
18



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







-
-
-






+
+







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

package policy

import (
	"context"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/query"
	"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
}
75
76
77
78
79
80
81
82

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

81
82
83
84
85
86
87
88







-
+







	return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
}

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

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

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

Changes to auth/policy/default.go.

10
11
12
13
14
15
16
17

18

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

36
37
38
39
40
41
42
43
44
45

46
47
48
49
50

51
52

53
54

55
56
57

58
10
11
12
13
14
15
16

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

36
37
38
39
40
41
42
43
44
45

46
47
48
49
50

51
52

53
54

55
56
57

58
59







-
+

+
















-
+









-
+




-
+

-
+

-
+


-
+

// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

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

type defaultPolicy struct {
	manager auth.AuthzManager
}

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

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

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

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

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

Changes to auth/policy/owner.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
10
11
12
13
14
15
16

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

40
41
42
43
44
45
46
47







-
+


+



















-
+







// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

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

type ownerPolicy struct {
	manager    auth.AuthzManager
	authConfig config.AuthConfig
	pre        auth.Policy
}

func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanCreate(user, newMeta) {
		return false
	}
	return o.userIsOwner(user) || o.userCanCreate(user, newMeta)
}

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

func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool {
	// No need to call o.pre.CanRead(user, meta), because it will always return true.
58
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
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







-
+














-
-
-
-
+
+
+
+
















-
+







		return false
	case meta.VisibilityPublic:
		return true
	}
	if user == nil {
		return false
	}
	if _, ok := m.Get(meta.KeyUserID); ok {
	if _, ok := m.Get(api.KeyUserID); ok {
		// Only the user can read its own zettel
		return user.Zid == m.Zid
	}
	switch o.manager.GetUserRole(user) {
	case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner:
		return true
	case meta.UserRoleCreator:
		return vis == meta.VisibilityCreator
	default:
		return false
	}
}

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

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

152
153
154
155
145
146
147
148
149
150
151

152
153
154
155
156







-
+




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

Changes to auth/policy/policy.go.

11
12
13
14
15
16
17
18
19
20

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

18
19
20
21
22
23
24
25
26
27







-


+







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

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

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

// newPolicy creates a policy based on given constraints.
func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy {
	var pol auth.Policy
	if manager.IsReadonly() {
		pol = &roPolicy{}

Changes to auth/policy/policy_test.go.

13
14
15
16
17
18
19
20
21
22




23
24
25
26
27
28
29
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27
28
29
30







-
-
-
+
+
+
+








package policy

import (
	"fmt"
	"testing"

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

func TestPolicies(t *testing.T) {
	t.Parallel()
	testScene := []struct {
		readonly bool
		withAuth bool
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
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







-
-
+
+












-
-
+
+







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

type authConfig struct{ simple, expert bool }

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

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

func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly bool) {
	t.Helper()
	anonUser := newAnon()
258
259
260
261
262
263
264
265

266
267
268
269
270
271
272
259
260
261
262
263
264
265

266
267
268
269
270
271
272
273







-
+







	zettel := newZettel()
	publicZettel := newPublicZettel()
	loginZettel := newLoginZettel()
	ownerZettel := newOwnerZettel()
	expertZettel := newExpertZettel()
	userZettel := newUserZettel()
	writerNew := writer.Clone()
	writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, ""))
	writerNew.Set(api.KeyUserRole, owner.GetDefault(api.KeyUserRole, ""))
	roFalse := newRoFalseZettel()
	roTrue := newRoTrueZettel()
	roReader := newRoReaderZettel()
	roWriter := newRoWriterZettel()
	roOwner := newRoOwnerZettel()
	notAuthNotReadonly := !withAuth && !readonly
	testCases := []struct {
520
521
522
523
524
525
526
527
528
529



530
531
532
533
534
535
536



537
538
539
540
541
542
543



544
545
546
547
548
549
550



551
552
553
554
555
556
557



558
559
560
561
562

563
564
565
566
567
568


569
570
571
572
573
574


575
576
577
578
579
580


581
582
583
584
585
586


587
588
589
590
591
592


593
594
595
596
597
598


599
600
601
602
603
604


605
606
607
608
609
610


611
612
613
614
615
616


617
618
619
620
621
622


623
624
625
626
627
628


629
630
521
522
523
524
525
526
527



528
529
530
531
532
533
534



535
536
537
538
539
540
541



542
543
544
545
546
547
548



549
550
551
552
553
554
555



556
557
558
559
560
561
562

563
564
565
566
567


568
569
570
571
572
573


574
575
576
577
578
579


580
581
582
583
584
585


586
587
588
589
590
591


592
593
594
595
596
597


598
599
600
601
602
603


604
605
606
607
608
609


610
611
612
613
614
615


616
617
618
619
620
621


622
623
624
625
626
627


628
629
630
631







-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
-
-
+
+
+




-
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+




-
-
+
+


	visZid     = id.Zid(1023)
	userZid    = id.Zid(1025)
)

func newAnon() *meta.Meta { return nil }
func newCreator() *meta.Meta {
	user := meta.New(creatorZid)
	user.Set(meta.KeyTitle, "Creator")
	user.Set(meta.KeyUserID, "ceator")
	user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator)
	user.Set(api.KeyTitle, "Creator")
	user.Set(api.KeyUserID, "ceator")
	user.Set(api.KeyUserRole, api.ValueUserRoleCreator)
	return user
}
func newReader() *meta.Meta {
	user := meta.New(readerZid)
	user.Set(meta.KeyTitle, "Reader")
	user.Set(meta.KeyUserID, "reader")
	user.Set(meta.KeyUserRole, meta.ValueUserRoleReader)
	user.Set(api.KeyTitle, "Reader")
	user.Set(api.KeyUserID, "reader")
	user.Set(api.KeyUserRole, api.ValueUserRoleReader)
	return user
}
func newWriter() *meta.Meta {
	user := meta.New(writerZid)
	user.Set(meta.KeyTitle, "Writer")
	user.Set(meta.KeyUserID, "writer")
	user.Set(meta.KeyUserRole, meta.ValueUserRoleWriter)
	user.Set(api.KeyTitle, "Writer")
	user.Set(api.KeyUserID, "writer")
	user.Set(api.KeyUserRole, api.ValueUserRoleWriter)
	return user
}
func newOwner() *meta.Meta {
	user := meta.New(ownerZid)
	user.Set(meta.KeyTitle, "Owner")
	user.Set(meta.KeyUserID, "owner")
	user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner)
	user.Set(api.KeyTitle, "Owner")
	user.Set(api.KeyUserID, "owner")
	user.Set(api.KeyUserRole, api.ValueUserRoleOwner)
	return user
}
func newOwner2() *meta.Meta {
	user := meta.New(owner2Zid)
	user.Set(meta.KeyTitle, "Owner 2")
	user.Set(meta.KeyUserID, "owner-2")
	user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner)
	user.Set(api.KeyTitle, "Owner 2")
	user.Set(api.KeyUserID, "owner-2")
	user.Set(api.KeyUserRole, api.ValueUserRoleOwner)
	return user
}
func newZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Any Zettel")
	m.Set(api.KeyTitle, "Any Zettel")
	return m
}
func newPublicZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Public Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
	m.Set(api.KeyTitle, "Public Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityPublic)
	return m
}
func newCreatorZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Creator Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator)
	m.Set(api.KeyTitle, "Creator Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityCreator)
	return m
}
func newLoginZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Login Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin)
	m.Set(api.KeyTitle, "Login Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}
func newOwnerZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Owner Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner)
	m.Set(api.KeyTitle, "Owner Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityOwner)
	return m
}
func newExpertZettel() *meta.Meta {
	m := meta.New(visZid)
	m.Set(meta.KeyTitle, "Expert Zettel")
	m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
	m.Set(api.KeyTitle, "Expert Zettel")
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m
}
func newRoFalseZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "No r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueFalse)
	m.Set(api.KeyTitle, "No r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueFalse)
	return m
}
func newRoTrueZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "A r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueTrue)
	m.Set(api.KeyTitle, "A r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueTrue)
	return m
}
func newRoReaderZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Reader r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueUserRoleReader)
	m.Set(api.KeyTitle, "Reader r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueUserRoleReader)
	return m
}
func newRoWriterZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Writer r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter)
	m.Set(api.KeyTitle, "Writer r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueUserRoleWriter)
	return m
}
func newRoOwnerZettel() *meta.Meta {
	m := meta.New(zettelZid)
	m.Set(meta.KeyTitle, "Owner r/o Zettel")
	m.Set(meta.KeyReadOnly, meta.ValueUserRoleOwner)
	m.Set(api.KeyTitle, "Owner r/o Zettel")
	m.Set(api.KeyReadOnly, api.ValueUserRoleOwner)
	return m
}
func newUserZettel() *meta.Meta {
	m := meta.New(userZid)
	m.Set(meta.KeyTitle, "Any User")
	m.Set(meta.KeyUserID, "any")
	m.Set(api.KeyTitle, "Any User")
	m.Set(api.KeyUserID, "any")
	return m
}

Changes to auth/policy/readonly.go.

9
10
11
12
13
14
15
16

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

16
17
18
19
20
21
22
23







-
+







//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

import "t73f.de/r/zsc/domain/meta"
import "zettelstore.de/z/zettel/meta"

type roPolicy struct{}

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

Changes to box/box.go.

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





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





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







-
-
-
-
-
+
+
+
+
+







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

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

// BaseBox is implemented by all Zettel boxes.
type BaseBox interface {
	// Location returns some information where the box is located.
	// Format is dependent of the box.
	Location() string
128
129
130
131
132
133
134
135

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

135
136
137
138
139
140
141
142







-
+








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

	// FetchZids returns the set of all zettel identifer managed by the box.
	FetchZids(ctx context.Context) (*idset.Set, error)
	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)
286
287
288
289
290
291
292
293

294
295
296
297

298
299
300
301
302
303
304
286
287
288
289
290
291
292

293
294
295
296

297
298
299
300
301
302
303
304







-
+



-
+







				err.Op, err.Zid)
		}
		return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op)
	}
	if err.Zid.IsValid() {
		return fmt.Sprintf(
			"operation %q on zettel %v not allowed for user %v/%v",
			err.Op, err.Zid, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid)
			err.Op, err.Zid, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid)
	}
	return fmt.Sprintf(
		"operation %q not allowed for user %v/%v",
		err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid)
		err.Op, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid)
}

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

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

Changes to box/compbox/compbox.go.

14
15
16
17
18
19
20
21

22
23
24
25
26
27
28


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

21

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







-
+
-






+
+







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

import (
	"context"
	"net/url"

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

func init() {
	manager.Register(
		" comp",
		func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return getCompBox(cdata.Number, cdata.Enricher), nil
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
















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
















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







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







}

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

// Get returns the one program box.
func getCompBox(boxNumber int, mf box.Enricher) *compBox {
	return &compBox{
		log: kernel.Main.GetLogger(kernel.BoxService).Clone().
			Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(),
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
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







-
+




-
-
+
+

-
-
-
+
+
+

-
-
-
-
+
+
+
+


	st.ReadOnly = true
	st.Zettel = len(myZettel)
	cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

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

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

Changes to box/compbox/config.go.

13
14
15
16
17
18
19
20
21


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


36
37
38
39
40

41
42

43
44

45
46
47
48
49
50
51
13
14
15
16
17
18
19


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



33
34
35
36

37

38
39

40
41

42
43
44
45
46
47
48
49







-
-
+
+











-
-
-
+
+


-

-
+

-
+

-
+








package compbox

import (
	"bytes"
	"context"

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

func genConfigZettelM(zid id.Zid) *meta.Meta {
	if myConfig == nil {
		return nil
	}
	return getTitledMeta(zid, "Zettelstore Startup Configuration")
}

func genConfigZettelC(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	second := false
	for key, val := range myConfig.All() {
		if second {
	for i, p := range myConfig.Pairs() {
		if i > 0 {
			buf.WriteByte('\n')
		}
		second = true
		buf.WriteString("; ''")
		buf.WriteString(key)
		buf.WriteString(p.Key)
		buf.WriteString("''")
		if val != "" {
		if p.Value != "" {
			buf.WriteString("\n: ``")
			for _, r := range val {
			for _, r := range p.Value {
				if r == '`' {
					buf.WriteByte('\\')
				}
				buf.WriteRune(r)
			}
			buf.WriteString("``")
		}

Changes to box/compbox/keys.go.

14
15
16
17
18
19
20
21
22
23




24
25
26
27
28
29


30
31
32
33
34
35
36
14
15
16
17
18
19
20



21
22
23
24
25
26
27
28


29
30
31
32
33
34
35
36
37







-
-
-
+
+
+
+




-
-
+
+







package compbox

import (
	"bytes"
	"context"
	"fmt"

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

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

func genKeysC(context.Context, *compBox) []byte {
	keys := meta.GetSortedKeyDescriptions()
	var buf bytes.Buffer
	buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n")

Changes to box/compbox/log.go.

13
14
15
16
17
18
19
20
21
22




23
24
25
26
27
28


29
30
31
32
33
34
35
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27


28
29
30
31
32
33
34
35
36







-
-
-
+
+
+
+




-
-
+
+








package compbox

import (
	"bytes"
	"context"

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

func genLogM(zid id.Zid) *meta.Meta {
	m := getTitledMeta(zid, "Zettelstore Log")
	m.Set(meta.KeySyntax, meta.ValueSyntaxText)
	m.Set(meta.KeyModified, meta.Value(kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout)))
	m.Set(api.KeySyntax, meta.SyntaxText)
	m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout))
	return m
}

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

Changes to box/compbox/manager.go.

14
15
16
17
18
19
20
21
22
23



24
25
26
27
28
29
30
14
15
16
17
18
19
20



21
22
23
24
25
26
27
28
29
30







-
-
-
+
+
+







package compbox

import (
	"bytes"
	"context"
	"fmt"

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

func genManagerM(zid id.Zid) *meta.Meta {
	return getTitledMeta(zid, "Zettelstore Box Manager")
}

func genManagerC(context.Context, *compBox) []byte {

Changes to box/compbox/memory.go.

16
17
18
19
20
21
22
23
24
25



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



23
24
25
26
27
28
29
30
31
32







-
-
-
+
+
+







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

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

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

func genMemoryC(context.Context, *compBox) []byte {

Changes to box/compbox/parser.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
16
17
18
19
20
21
22

23

24
25
26
27
28
29
30
31


32
33
34
35
36
37
38
39
40







-
+
-


+
+




-
-
+
+







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

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

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

func genParserC(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n")
	syntaxes := parser.GetSyntaxes()

Changes to box/compbox/sx.go.

15
16
17
18
19
20
21
22
23


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


22
23
24
25
26
27
28
29
30







-
-
+
+








import (
	"bytes"
	"context"
	"fmt"

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

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

func genSxC(context.Context, *compBox) []byte {

Changes to box/compbox/version.go.

12
13
14
15
16
17
18
19
20
21




22
23
24
25
26
27


28
29
30
31
32
33
34
12
13
14
15
16
17
18



19
20
21
22
23
24
25
26


27
28
29
30
31
32
33
34
35







-
-
-
+
+
+
+




-
-
+
+







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

package compbox

import (
	"context"

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

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

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

Changes to box/constbox/base.sxn.

36
37
38
39
40
41
42


43

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

45
46
47
48
49
50
51
52







+
+
-
+







            `((a (@ (href ,login-url)) "Login"))
          )
      )))
    )
    (div (@ (class "zs-dropdown"))
      (button "Lists")
      (nav (@ (class "zs-dropdown-content"))
        (a (@ (href ,list-zettel-url)) "List Zettel")
        (a (@ (href ,list-roles-url)) "List Roles")
        ,@list-urls
        (a (@ (href ,list-tags-url)) "List Tags")
        ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh")))
    ))
    ,@(if new-zettel-links
      `((div (@ (class "zs-dropdown"))
        (button "New")
        (nav (@ (class "zs-dropdown-content"))
          ,@(map wui-link new-zettel-links)

Changes to box/constbox/constbox.go.

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29


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

22

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







-
+
-






+
+







package constbox

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

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

func init() {
	manager.Register(
		" const",
		func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &constBox{
111
112
113
114
115
116
117
118

119
120
121
122
123
124





125
126
127

128
129
130
131
132
133
134
135
136








137
138
139

140
141
142
143
144
145
146
147







148
149
150

151
152
153
154
155
156
157
158
159








160
161
162

163
164
165
166
167
168
169






170
171
172

173
174
175
176
177
178
179






180
181
182

183
184
185
186
187
188
189






190
191
192

193
194
195
196
197
198
199






200
201
202

203
204
205
206
207
208
209






210
211
212

213
214
215
216
217
218
219






220
221
222

223
224
225
226
227
228
229






230
231
232

233
234
235
236
237
238
239






240
241
242

243
244
245
246
247
248
249
250







251
252
253

254
255
256
257
258
259
260
261
262








263
264
265

266
267
268
269
270
271
272
273







274
275
276

277
278
279
280
281
282
283






284
285
286

287
288
289
290
291
292





293
294
295

296
297
298
299
300
301
302






303
304
305
306
307
308
309
310
311
312
313
314
315

316
317
318
319
320
321
322
323







324
325
326


327
328
329
330
331
332
333
334







335
336
337

338
339
340
341
342
343
344
345







346
347
348

349
350
351
352
353
354
355
356







357
358
359

360
361
362
363
364
365
366
367
368








369
370
371

372
373
374
375
376
377
378






379
380
381

382
383
384
385
386
387
388






389
390
391

392
393
394
395
396
397
398






399
400
401

402
403
404
405
406
407
408






409
410
411

412
413
414
415
416
417
418






419
420
421

422
423
424
425
426
427





428
429
430
431
432
433
434
435
112
113
114
115
116
117
118

119
120





121
122
123
124
125
126
127

128
129








130
131
132
133
134
135
136
137
138
139

140
141







142
143
144
145
146
147
148
149
150

151
152








153
154
155
156
157
158
159
160
161
162

163
164






165
166
167
168
169
170
171
172

173
174






175
176
177
178
179
180
181
182

183
184






185
186
187
188
189
190
191
192

193
194






195
196
197
198
199
200
201
202

203
204






205
206
207
208
209
210
211
212

213
214






215
216
217
218
219
220
221
222

223
224






225
226
227
228
229
230
231
232

233
234






235
236
237
238
239
240
241
242

243
244







245
246
247
248
249
250
251
252
253

254
255








256
257
258
259
260
261
262
263
264
265

266
267







268
269
270
271
272
273
274
275
276

277
278






279
280
281
282
283
284
285
286

287
288





289
290
291
292
293
294
295

296
297






298
299
300
301
302
303
304
305











306
307







308
309
310
311
312
313
314
315


316
317
318







319
320
321
322
323
324
325
326
327

328
329







330
331
332
333
334
335
336
337
338

339
340







341
342
343
344
345
346
347
348
349

350
351








352
353
354
355
356
357
358
359
360
361

362
363






364
365
366
367
368
369
370
371

372
373






374
375
376
377
378
379
380
381

382
383






384
385
386
387
388
389
390
391

392
393






394
395
396
397
398
399
400
401

402
403






404
405
406
407
408
409
410
411

412
413





414
415
416
417
418

419
420
421
422
423
424
425







-
+

-
-
-
-
-
+
+
+
+
+


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

-
-
-
-
-
+
+
+
+
+


-
+

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


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

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

-
-
+
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

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


-
+

-
-
-
-
-
+
+
+
+
+
-







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

var constZettelMap = map[id.Zid]constZettel{
	id.ZidConfiguration: {
	id.ConfigurationZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Runtime Configuration",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxNone,
			meta.KeyCreated:    "20200804111624",
			meta.KeyVisibility: meta.ValueVisibilityOwner,
			api.KeyTitle:      "Zettelstore Runtime Configuration",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxNone,
			api.KeyCreated:    "20200804111624",
			api.KeyVisibility: api.ValueVisibilityOwner,
		},
		zettel.NewContent(nil)},
	id.ZidLicense: {
	id.MustParse(api.ZidLicense): {
		constHeader{
			meta.KeyTitle:      "Zettelstore License",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxText,
			meta.KeyCreated:    "20210504135842",
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyModified:   "20220131153422",
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			api.KeyTitle:      "Zettelstore License",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxText,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyModified:   "20220131153422",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentLicense)},
	id.ZidAuthors: {
	id.MustParse(api.ZidAuthors): {
		constHeader{
			meta.KeyTitle:      "Zettelstore Contributors",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyCreated:    "20210504135842",
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityLogin,
			api.KeyTitle:      "Zettelstore Contributors",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentContributors)},
	id.ZidDependencies: {
	id.MustParse(api.ZidDependencies): {
		constHeader{
			meta.KeyTitle:      "Zettelstore Dependencies",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeyCreated:    "20210504135842",
			meta.KeyModified:   "20250212202400",
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20240418095500",
		},
		zettel.NewContent(contentDependencies)},
	id.ZidBaseTemplate: {
	id.BaseTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155100",
			meta.KeyModified:   "20241227212000",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155100",
			api.KeyModified:   "20240219145300",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.ZidLoginTemplate: {
	id.LoginTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZidZettelTemplate: {
	id.ZettelTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155300",
			meta.KeyModified:   "20250214153100",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20241127170400",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentZettelSxn)},
	id.ZidInfoTemplate: {
	id.InfoTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20241127170500",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Info HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20241127170500",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.ZidFormTemplate: {
	id.FormTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},
	id.ZidDeleteTemplate: {
	id.DeleteTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20241127170530",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20241127170530",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ZidListTemplate: {
	id.ListTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230704122100",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230704122100",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ZidErrorTemplate: {
	id.ErrorTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Error HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20210305133215",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Error HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20210305133215",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentErrorSxn)},
	id.ZidSxnStart: {
	id.StartSxnZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Sxn Start Code",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230824160700",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeyPrecursor:  id.ZidSxnBase.String(),
			api.KeyTitle:      "Zettelstore Sxn Start Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230824160700",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnBase),
		},
		zettel.NewContent(contentStartCodeSxn)},
	id.ZidSxnBase: {
	id.BaseSxnZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Sxn Base Code",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230619132800",
			meta.KeyModified:   "20241118173500",
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeyPrecursor:  id.ZidSxnPrelude.String(),
			api.KeyTitle:      "Zettelstore Sxn Base Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",
			api.KeyModified:   "20241118173500",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnPrelude),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.ZidSxnPrelude: {
	id.PreludeSxnZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Sxn Prelude",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20231006181700",
			meta.KeyModified:   "20240222121200",
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			api.KeyTitle:      "Zettelstore Sxn Prelude",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20231006181700",
			api.KeyModified:   "20240222121200",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentPreludeSxn)},
	id.ZidBaseCSS: {
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxCSS,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240827143500",
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240827143500",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentBaseCSS)},
	id.ZidUserCSS: {
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			meta.KeyTitle:      "Zettelstore User CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxCSS,
			meta.KeyCreated:    "20210622110143",
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			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.ZidEmoji: {
	id.EmojiZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Generic Emoji",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxGif,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyCreated:    "20210504175807",
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			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.ZidTOCListsMenu: {
		constHeader{
			meta.KeyTitle:      "Lists Menu",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyCreated:    "20241223205400",
			meta.KeyVisibility: meta.ValueVisibilityPublic,
		},
		zettel.NewContent(contentMenuListsZettel)},
	id.ZidTOCNewTemplate: {
	id.TOCNewTemplateZid: {
		constHeader{
			meta.KeyTitle:      "New Menu",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyCreated:    "20210217161829",
			meta.KeyModified:   "20231129111800",
			meta.KeyVisibility: meta.ValueVisibilityCreator,
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",
			api.KeyModified:   "20231129111800",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentMenuNewZettel)},
	id.ZidTemplateNewZettel: {
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			meta.KeyTitle:                 "New Zettel",
			meta.KeyRole:                  meta.ValueRoleConfiguration,
			meta.KeySyntax:                meta.ValueSyntaxZmk,
			meta.KeyCreated:               "20201028185209",
			meta.KeyModified:              "20230929132900",
			meta.NewPrefix + meta.KeyRole: meta.ValueRoleZettel,
			meta.KeyVisibility:            meta.ValueVisibilityCreator,
			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.ZidTemplateNewRole: {
	id.MustParse(api.ZidTemplateNewRole): {
		constHeader{
			meta.KeyTitle:                  "New Role",
			meta.KeyRole:                   meta.ValueRoleConfiguration,
			meta.KeySyntax:                 meta.ValueSyntaxZmk,
			meta.KeyCreated:                "20231129110800",
			meta.NewPrefix + meta.KeyRole:  meta.ValueRoleRole,
			meta.NewPrefix + meta.KeyTitle: "",
			meta.KeyVisibility:             meta.ValueVisibilityCreator,
			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.ZidTemplateNewTag: {
	id.MustParse(api.ZidTemplateNewTag): {
		constHeader{
			meta.KeyTitle:                  "New Tag",
			meta.KeyRole:                   meta.ValueRoleConfiguration,
			meta.KeySyntax:                 meta.ValueSyntaxZmk,
			meta.KeyCreated:                "20230929132400",
			meta.NewPrefix + meta.KeyRole:  meta.ValueRoleTag,
			meta.NewPrefix + meta.KeyTitle: "#",
			meta.KeyVisibility:             meta.ValueVisibilityCreator,
			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.ZidTemplateNewUser: {
	id.MustParse(api.ZidTemplateNewUser): {
		constHeader{
			meta.KeyTitle:                       "New User",
			meta.KeyRole:                        meta.ValueRoleConfiguration,
			meta.KeySyntax:                      meta.ValueSyntaxNone,
			meta.KeyCreated:                     "20201028185209",
			meta.NewPrefix + meta.KeyCredential: "",
			meta.NewPrefix + meta.KeyUserID:     "",
			meta.NewPrefix + meta.KeyUserRole:   meta.ValueUserRoleReader,
			meta.KeyVisibility:                  meta.ValueVisibilityOwner,
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.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.ZidRoleZettelZettel: {
	id.MustParse(api.ZidRoleZettelZettel): {
		constHeader{
			meta.KeyTitle:      meta.ValueRoleZettel,
			meta.KeyRole:       meta.ValueRoleRole,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyCreated:    "20231129161400",
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyVisibility: meta.ValueVisibilityLogin,
			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.ZidRoleConfigurationZettel: {
	id.MustParse(api.ZidRoleConfigurationZettel): {
		constHeader{
			meta.KeyTitle:      meta.ValueRoleConfiguration,
			meta.KeyRole:       meta.ValueRoleRole,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyCreated:    "20241213103100",
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyVisibility: meta.ValueVisibilityLogin,
			api.KeyTitle:      api.ValueRoleConfiguration,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20241213103100",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleConfiguration)},
	id.ZidRoleRoleZettel: {
	id.MustParse(api.ZidRoleRoleZettel): {
		constHeader{
			meta.KeyTitle:      meta.ValueRoleRole,
			meta.KeyRole:       meta.ValueRoleRole,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyCreated:    "20231129162900",
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyVisibility: meta.ValueVisibilityLogin,
			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.ZidRoleTagZettel: {
	id.MustParse(api.ZidRoleTagZettel): {
		constHeader{
			meta.KeyTitle:      meta.ValueRoleTag,
			meta.KeyRole:       meta.ValueRoleRole,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyCreated:    "20231129162000",
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyVisibility: meta.ValueVisibilityLogin,
			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.ZidAppDirectory: {
	id.MustParse(api.ZidAppDirectory): {
		constHeader{
			meta.KeyTitle:      "Zettelstore Application Directory",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxNone,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyCreated:    "20240703235900",
			meta.KeyVisibility: meta.ValueVisibilityLogin,
			api.KeyTitle:      "Zettelstore Application Directory",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxNone,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20240703235900",
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(nil)},
	id.ZidDefaultHome: {
	id.DefaultHomeZid: {
		constHeader{
			meta.KeyTitle:    "Home",
			meta.KeyRole:     meta.ValueRoleZettel,
			meta.KeySyntax:   meta.ValueSyntaxZmk,
			meta.KeyLang:     meta.ValueLangEN,
			meta.KeyCreated:  "20210210190757",
			api.KeyTitle:   "Home",
			api.KeyRole:    api.ValueRoleZettel,
			api.KeySyntax:  meta.SyntaxZmk,
			api.KeyLang:    api.ValueLangEN,
			api.KeyCreated: "20210210190757",
			meta.KeyModified: "20241216105800",
		},
		zettel.NewContent(contentHomeZettel)},
}

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

474
475
476
477
478
479
480
481
482
483
484
485


486
487
488
489
490
491
492
464
465
466
467
468
469
470





471
472
473
474
475
476
477
478
479







-
-
-
-
-
+
+








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

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

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

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

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

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

Changes to box/constbox/dependencies.zettel.

126
127
128
129
130
131
132
133

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

133
134
135
136
137
138
139
140
141
142


143
144
145
146







-
+









-
-




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

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

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

Changes to box/constbox/home.zettel.

1
2
3
4
5
6
7





8

9

10
11
12
13
14
15
16
17
18
19


20
21
22
23
24
25
26

27
28
29
30
31
32
33
1
2





3
4
5
6
7
8
9

10
11
12
13
14
15
16
17
18


19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
34


-
-
-
-
-
+
+
+
+
+

+
-
+








-
-
+
+






-
+







=== Thank you for using Zettelstore!

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

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

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

Additionally, you have to describe, what you did before that error occurs
and what you expected instead.
Additionally, you have to describe, what you have done before that error occurs
and what you have expected instead.
Please do not forget to include the error message, if there is one.

Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"".
Otherwise, only some zettel are linked.
To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]:
please set the metadata value of the key ''expert-mode'' to true.
To do so, enter the string ''expert-mode:true'' inside the edit view of the metadata.
To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata.

=== Information about this zettel
This zettel is your home zettel.
It is part of the Zettelstore software itself.
Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel.

You can change the content of this zettel by clicking on ""Edit"" above.

Deleted box/constbox/menu_lists.zettel.

1
2
3
4
5
6
7







-
-
-
-
-
-
-
This zettel lists all entries of the ""Lists"" menu.

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

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

Deleted box/constbox/menu_new.zettel.

1
2
3
4
5
6






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

Added box/constbox/newtoc.zettel.







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

Changes to box/constbox/zettel.sxn.

20
21
22
23
24
25
26
27
28
29
30

31
32
33
34
35
36

37
38
39
40
41
42

43
44
45
20
21
22
23
24
25
26

27

28
29
30
31
32
33
34

35
36
37
38


39
40
41
42
43







-

-

+





-
+



-
-

+



      (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-binding))
      ,@(if superior-refs `((br) "Superior: " ,superior-refs))
      ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
      ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs))
      ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
      ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs))
      ,@(ROLE-DEFAULT-heading (current-binding))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links sequel-links back-links successor-links subordinate-links)
  ,@(if (or folge-links sequel-links back-links successor-links)
    `((nav
      ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
      ,@(if sequel-links `((details (@ (,sequel-open)) (summary "Sequel") (ul ,@(map wui-item-link sequel-links)))))
      ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-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.

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


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


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







-
-







+
+







	"context"
	"errors"
	"net/url"
	"os"
	"path/filepath"
	"sync"

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

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		var log *logger.Logger
		if krnl := kernel.Main; krnl != nil {
			log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child()

Changes to box/dirbox/service.go.

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


32
33
34
35
36
37
38
17
18
19
20
21
22
23


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







-
-






+
+







	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"time"

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

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)

Changes to box/filebox/filebox.go.

16
17
18
19
20
21
22
23

24
25
26
27


28
29
30
31
32
33
34
16
17
18
19
20
21
22

23

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







-
+
-



+
+








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

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

func init() {
	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		path := getFilepathFromURL(u)
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".zip" {
54
55
56
57
58
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
55
56
57
58
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







-
+



-
+




-
+





-
+






-
+

-
+



-
+




-
+


	fileName := filepath.Join(components...)
	if len(components) > 0 && components[0] == "" {
		return "/" + fileName
	}
	return fileName
}

var alternativeSyntax = map[string]meta.Value{
var alternativeSyntax = map[string]string{
	"htm": "html",
}

func calculateSyntax(ext string) meta.Value {
func calculateSyntax(ext string) string {
	ext = strings.ToLower(ext)
	if syntax, ok := alternativeSyntax[ext]; ok {
		return syntax
	}
	return meta.Value(ext)
	return ext
}

// CalcDefaultMeta returns metadata with default values for the given entry.
func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta {
	m := meta.New(zid)
	m.Set(meta.KeySyntax, calculateSyntax(ext))
	m.Set(api.KeySyntax, calculateSyntax(ext))
	return m
}

// CleanupMeta enhances the given metadata.
func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) {
	if inMeta {
		if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
			dm := CalcDefaultMeta(zid, ext)
			syntax, ok = dm.Get(meta.KeySyntax)
			syntax, ok = dm.Get(api.KeySyntax)
			if !ok {
				panic("Default meta must contain syntax")
			}
			m.Set(meta.KeySyntax, syntax)
			m.Set(api.KeySyntax, syntax)
		}
	}

	if len(uselessFiles) > 0 {
		m.Set(meta.KeyUselessFiles, meta.Value(strings.Join(uselessFiles, " ")))
		m.Set(api.KeyUselessFiles, strings.Join(uselessFiles, " "))
	}
}

Changes to box/filebox/zipbox.go.

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


31
32
33
34
35
36
37
16
17
18
19
20
21
22


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







-
-






+
+







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

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

type zipBox struct {
	log      *logger.Logger
	number   int
	name     string
	enricher box.Enricher

Changes to box/helper.go.

14
15
16
17
18
19
20
21

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

21
22
23
24
25
26
27
28







-
+







package box

import (
	"net/url"
	"strconv"
	"time"

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

// GetNewZid calculates a new and unused zettel identifier, based on the current date and time.
func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) {
	withSeconds := false
	for range 90 { // Must be completed within 9 seconds (less than web/server.writeTimeout)
		zid := id.New(withSeconds)

Changes to box/manager/anteroom.go.

12
13
14
15
16
17
18
19

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

34
35
36
37
38
39
40
12
13
14
15
16
17
18

19

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

32
33
34
35
36
37
38
39







-
+
-












-
+







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

package manager

import (
	"sync"

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

type arAction int

const (
	arNothing arAction = iota
	arReload
	arZettel
)

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

type anteroomQueue struct {
	mx      sync.Mutex
	first   *anteroom
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
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







-
+










-
+







	ar.last = room
}

func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom {
	if zid == id.Invalid {
		panic(zid)
	}
	waiting := idset.NewCap(max(ar.maxLoad, 100), 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 *idset.Set) {
func (ar *anteroomQueue) Reload(allZids *id.Set) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.deleteReloadedRooms()

	if !allZids.IsEmpty() {
		ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: allZids.Length(), reload: true}
		if ar.first.next == nil {

Changes to box/manager/anteroom_test.go.

12
13
14
15
16
17
18
19

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

19

20
21
22
23
24
25
26







-
+
-







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

package manager

import (
	"testing"

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

func TestSimple(t *testing.T) {
	t.Parallel()
	ar := newAnteroomQueue(2)
	ar.EnqueueZettel(id.Zid(1))
	action, zid, lastReload := ar.Dequeue()
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
58
59
60
61
62
63
64

65
66
67
68
69
70
71
72







-
+







	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(idset.New(3, 4))
	ar.Reload(id.NewSet(3, 4))
	ar.EnqueueZettel(id.Zid(5))
	ar.EnqueueZettel(id.Zid(5))
	if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ {
		t.Errorf("Expected 2 rooms")
	}
	action, zid1, _ := ar.Dequeue()
	if action != arZettel {
86
87
88
89
90
91
92
93

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

92
93
94
95
96
97
98
99







-
+







	}
	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(idset.New(id.Zid(6)))
	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)

Changes to box/manager/box.go.

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


27
28
29
30
31
32
33
14
15
16
17
18
19
20



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







-
-
-



+
+







package manager

import (
	"context"
	"errors"
	"strings"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Conatains all box.Box related functions

// Location returns some information where the box is located.
func (mgr *Manager) Location() string {
	if len(mgr.boxes) <= 2 {
62
63
64
65
66
67
68
69

70
71
72
73

74
75
76
77
78
79
80
61
62
63
64
65
66
67

68
69
70
71

72
73
74
75
76
77
78
79







-
+



-
+







	if err := mgr.checkContinue(ctx); err != nil {
		return id.Invalid, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox {
		ztl.Meta = mgr.cleanMetaProperties(ztl.Meta)
		zid, err := box.CreateZettel(ctx, ztl)
		zidO, err := box.CreateZettel(ctx, ztl)
		if err == nil {
			mgr.idxUpdateZettel(ctx, ztl)
		}
		return zid, err
		return zidO, 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")
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
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







-
+








-
+






-
+








+
+
+
+
+
+







			result = append(result, z)
		}
	}
	return result, nil
}

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (*idset.Set, error) {
func (mgr *Manager) FetchZids(ctx context.Context) (*id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZids")
	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.fetchZids(ctx)
}
func (mgr *Manager) fetchZids(ctx context.Context) (*idset.Set, error) {
func (mgr *Manager) fetchZids(ctx context.Context) (*id.Set, error) {
	numZettel := 0
	for _, p := range mgr.boxes {
		var mbstats box.ManagedBoxStats
		p.ReadStats(&mbstats)
		numZettel += mbstats.Zettel
	}
	result := idset.NewCap(numZettel)
	result := id.NewSetCap(numZettel)
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, query.AlwaysIncluded)
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

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

func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel")
	if err := mgr.checkContinue(ctx); err != nil {
		return false
	}
	mgr.mgrMx.RLock()
189
190
191
192
193
194
195
196

197
198
199
200
201
202
203
194
195
196
197
198
199
200

201
202
203
204
205
206
207
208







-
+







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







-
-
+
+






-
+

-
+







-
+





-
-
-
+
+
+




			return true
		}
	}
	return false
}

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

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

Changes to box/manager/collect.go.

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

24
25
26
27

28
29
30
31
32
33

34
35
36
37
38
39
40




41
42
43
44
45
46
47
12
13
14
15
16
17
18


19
20
21
22
23
24
25

26
27
28
29
30
31

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







-
-



+



-
+





-
+







+
+
+
+







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

package manager

import (
	"strings"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/id/idset"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

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

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

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

func collectInlineIndexData(is *ast.InlineSlice, data *collectData) {
	ast.Walk(data, is)
}

func (data *collectData) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.VerbatimNode:
		data.addText(string(n.Content))
	case *ast.TranscludeNode:
		data.addRef(n.Ref)

Changes to box/manager/enrich.go.

13
14
15
16
17
18
19
20
21
22




23
24
25
26
27
28
29



30
31
32
33
34
35

36
37
38
39
40
41







42
43
44
45
46
47
48
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27
28


29
30
31
32
33
34
35
36

37
38
39
40



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







-
-
-
+
+
+
+





-
-
+
+
+





-
+



-
-
-
+
+
+
+
+
+
+








package manager

import (
	"context"
	"strconv"

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

// Enrich computes additional properties and updates the given metadata.
func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) {
	// Calculate computed, but stored values.
	if _, hasCreated := m.Get(meta.KeyCreated); !hasCreated {
		m.Set(meta.KeyCreated, computeCreated(m.Zid))
	_, hasCreated := m.Get(api.KeyCreated)
	if !hasCreated {
		m.Set(api.KeyCreated, computeCreated(m.Zid))
	}

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

func computeCreated(zid id.Zid) meta.Value {

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

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

70
71
72
73
74
75
76
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82







-
+







	zid /= 100
	day := zid % 100
	zid /= 100
	month := zid % 100
	year := zid / 100
	month, day = sanitizeMonthDay(year, month, day)
	created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds
	return meta.Value(created.String())
	return created.String()
}

func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) {
	if day < 1 {
		day = 1
	}
	if month < 1 {
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
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







-
+


-
-
-
+
+
+



-
-
-
+
+
+



-
-
-
+
+
+






			}
		}
	}
	return month, day
}

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

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

Changes to box/manager/indexer.go.

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


31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46

47
48
49
50
51
52
53
54
55
56
57

58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
15
16
17
18
19
20
21



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

34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
74







-
-
-






+
+




-
+










-
+










-
+










-
+








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

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

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

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







-
-
+
+





-
+

-
+


+
+
+

-
-
+
+



-
+



-
+







}

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

func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
	for key, val := range m.Computed() {
		descr := meta.GetDescription(key)
	for _, pair := range m.ComputedPairs() {
		descr := meta.GetDescription(pair.Key)
		if descr.IsProperty() {
			continue
		}
		switch descr.Type {
		case meta.TypeID:
			mgr.idxUpdateValue(ctx, descr.Inverse, string(val), zi)
			mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi)
		case meta.TypeIDSet:
			for val := range val.Fields() {
			for _, val := range meta.ListFromValue(pair.Value) {
				mgr.idxUpdateValue(ctx, descr.Inverse, val, zi)
			}
		case meta.TypeZettelmarkup:
			is := parser.ParseMetadata(pair.Value)
			collectInlineIndexData(&is, cData)
		case meta.TypeURL:
			if _, err := url.Parse(string(val)); err == nil {
				cData.urls.Add(string(val))
			if _, err := url.Parse(pair.Value); err == nil {
				cData.urls.Add(pair.Value)
			}
		default:
			if descr.Type.IsSet {
				for val := range val.Fields() {
				for _, val := range meta.ListFromValue(pair.Value) {
					idxCollectMetaValue(cData.words, val)
				}
			} else {
				idxCollectMetaValue(cData.words, string(val))
				idxCollectMetaValue(cData.words, pair.Value)
			}
		}
	}
}

func idxCollectMetaValue(stWords store.WordSet, value string) {
	if words := strfun.NormalizeWords(value); len(words) > 0 {
242
243
244
245
246
247
248
249

250
251
252
253
244
245
246
247
248
249
250

251
252
253
254
255







-
+




}

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

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

Changes to box/manager/manager.go.

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



34
35
36
37
38
39
40
17
18
19
20
21
22
23



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







-
-
-







+
+
+







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

	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/mapstore"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ConnectData contains all administration related values.
type ConnectData struct {
	Number   int // number of the box, starting with 1.
	Config   config.Config
	Enricher box.Enricher
89
90
91
92
93
94
95
96

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

96
97
98
99
100
101
102
103







-
+







	mgrMx        sync.RWMutex
	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys *set.Set[string] // Set of property key names
	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

121
122
123
124
125
126
127
128

129
130
131

132
133
134
135
136
137
138
121
122
123
124
125
126
127

128
129
130

131
132
133
134
135
136
137
138







-
+


-
+







	mgr.stateMx.RUnlock()
	return state
}

// New creates a new managing box.
func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) {
	descrs := meta.GetSortedKeyDescriptions()
	propertyKeys := set.New[string]()
	propertyKeys := make(strfun.Set, len(descrs))
	for _, kd := range descrs {
		if kd.IsProperty() {
			propertyKeys.Add(kd.Name)
			propertyKeys.Set(kd.Name)
		}
	}
	boxLog := kernel.Main.GetLogger(kernel.BoxService)
	mgr := &Manager{
		mgrLog:       boxLog.Clone().Str("box", "manager").Child(),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
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
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







-
+






-
+

-
+

-
+







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

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zidO id.Zid) {
	switch reason {
	case box.OnReady:
		return
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
		mgr.idxAr.EnqueueZettel(zidO)
	case box.OnDelete:
		mgr.idxAr.EnqueueZettel(zid)
		mgr.idxAr.EnqueueZettel(zidO)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zidO).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}

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

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

27
28

29
30


31
32
33
34
35
36
37



38
39
40
41
42
43
44
45


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

63
64
65
66
67
68
69
70

71
72
73
74
75
76
77

78
79
80
81
82
83
84
14
15
16
17
18
19
20

21
22
23
24

25


26
27
28
29
30
31
32
33
34



35
36
37
38
39
40
41
42
43


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

62
63
64
65
66
67
68
69

70
71
72
73
74
75
76

77
78
79
80
81
82
83
84







-




-
+
-
-
+


+
+




-
-
-
+
+
+






-
-
+
+
















-
+







-
+






-
+







// Package mapstore stored the index in main memory via a Go map.
package mapstore

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

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

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

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

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

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

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

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

func (ms *mapStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	ms.mx.RLock()
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
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







-
+




-
+



-
+





-
+





-
+







-
+


-
+




















-
+







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

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

	return addBackwardZids(result, zid, zi)
}

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

195
196
197
198
199
200
201
187
188
189
190
191
192
193

194
195
196
197
198
199
200
201







-
+







		}
	}
	return result
}

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







-
+

















-
+

-
+















-
+









-
-
-
+
+
+

-
+



-
+









-
+










-
+







		}
	}
	return result
}

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

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

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

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

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

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

	zi.meta = m
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
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







-
-
-
-
-
+
+
+
+
+




















-
-
+
+

-
-
-
+
+
+


















-
+





-
+

















-
+











-
+







		ms.idx[zidx.Zid] = zi
	}
	zi.optimize()
	return toCheck
}

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

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

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

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

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

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

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

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

		newRefs.ForEach(func(ref id.Zid) {
453
454
455
456
457
458
459
460

461
462
463
464
465
466

467
468
469
470
471
472
473
453
454
455
456
457
458
459

460
461
462
463
464
465

466
467
468
469
470
471
472
473







-
+





-
+







		return zi
	}
	zi := &zettelData{}
	ms.idx[zid] = zi
	return zi
}

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

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

	ms.deleteDeadSources(zid, zi)
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
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







-
+







-
+









-
+







			} else {
				ms.dead[ref] = drefs
			}
		}
	})
}

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

	var toCheck *idset.Set
	var toCheck *id.Set
	zi.backward.ForEach(func(ref id.Zid) {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = bzi.forward.Remove(zid)
			toCheck = toCheck.Add(ref)
		}
	})
	return toCheck
}

func (ms *mapStore) removeInverseMeta(zid id.Zid, key string, forward *idset.Set) {
func (ms *mapStore) removeInverseMeta(zid id.Zid, key string, forward *id.Set) {
	// Must only be called if ms.mx is write-locked!
	forward.ForEach(func(ref id.Zid) {
		bzi, ok := ms.idx[ref]
		if !ok || bzi.otherRefs == nil {
			return
		}
		bmr, ok := bzi.otherRefs[key]
590
591
592
593
594
595
596
597

598
599
600
601

602
603
604
605
606
607
608
590
591
592
593
594
595
596

597
598
599
600

601
602
603
604
605
606
607
608







-
+



-
+







}

func (ms *mapStore) dumpIndex(w io.Writer) {
	if len(ms.idx) == 0 {
		return
	}
	io.WriteString(w, "==== Zettel Index\n")
	zids := make([]id.Zid, 0, len(ms.idx))
	zids := make(id.Slice, 0, len(ms.idx))
	for id := range ms.idx {
		zids = append(zids, id)
	}
	slices.Sort(zids)
	zids.Sort()
	for _, id := range zids {
		fmt.Fprintln(w, "=====", id)
		zi := ms.idx[id]
		if !zi.dead.IsEmpty() {
			fmt.Fprintln(w, "* Dead:", zi.dead)
		}
		dumpSet(w, "* Forward:", zi.forward)
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
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







-
+



-
+






-
+







}

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

func dumpSet(w io.Writer, prefix string, s *idset.Set) {
func dumpSet(w io.Writer, prefix string, s *id.Set) {
	if !s.IsEmpty() {
		io.WriteString(w, prefix)
		s.ForEach(func(zid id.Zid) {
			io.WriteString(w, " ")
			w.Write(zid.Bytes())
		})
		fmt.Fprintln(w)
662
663
664
665
666
667
668
669

670
671
672
673
662
663
664
665
666
667
668

669
670
671
672
673







-
+




}

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

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

14
15
16
17
18
19
20
21

22
23
24


25
26
27
28
29
30
31
14
15
16
17
18
19
20

21



22
23
24
25
26
27
28
29
30







-
+
-
-
-
+
+







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

import (
	"context"
	"io"

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

// Stats records statistics about the store.
type Stats struct {
	// Zettel is the number of zettel managed by the indexer.
	Zettel int

48
49
50
51
52
53
54
55

56
57
58
59

60
61
62
63
64
65
66
67
68
69
47
48
49
50
51
52
53

54
55
56
57

58
59
60
61
62
63
64
65
66
67
68







-
+



-
+










	GetMeta(context.Context, id.Zid) (*meta.Meta, error)

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

	// UpdateReferences for a specific zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	UpdateReferences(context.Context, *ZettelIndex) *idset.Set
	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) *idset.Set
	DeleteZettel(context.Context, id.Zid) *id.Set

	// Optimize removes unneeded space.
	Optimize()

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

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

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

10
11
12
13
14
15
16
17

18
19

20
21
22
23
24
25
26
27
28





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



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

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

71
72
73
74
75
76

77
78
79

80
81
82
83

84
85
86
87
88
89
90
91
92
93
94
10
11
12
13
14
15
16

17


18
19
20
21
22





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



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

54
55
56
57
58
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







-
+
-
-
+




-
-
-
-
-
+
+
+
+
+









-
-
-
+
+
+














-
+














-
+





-
+


-
+



-
+











// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package store

import (
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/zettel/id"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

// ZettelIndex contains all index data of a zettel.
type ZettelIndex struct {
	Zid         id.Zid                // zid of the indexed zettel
	meta        *meta.Meta            // full metadata
	backrefs    *idset.Set            // set of back references
	inverseRefs map[string]*idset.Set // references of inverse keys
	deadrefs    *idset.Set            // set of dead references
	Zid         id.Zid             // zid of the indexed zettel
	meta        *meta.Meta         // full metadata
	backrefs    *id.Set            // set of back references
	inverseRefs map[string]*id.Set // references of inverse keys
	deadrefs    *id.Set            // set of dead references
	words       WordSet
	urls        WordSet
}

// NewZettelIndex creates a new zettel index.
func NewZettelIndex(m *meta.Meta) *ZettelIndex {
	return &ZettelIndex{
		Zid:         m.Zid,
		meta:        m,
		backrefs:    idset.New(),
		inverseRefs: make(map[string]*idset.Set),
		deadrefs:    idset.New(),
		backrefs:    id.NewSet(),
		inverseRefs: make(map[string]*id.Set),
		deadrefs:    id.NewSet(),
	}
}

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

// AddInverseRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) {
	if zids, ok := zi.inverseRefs[key]; ok {
		zids.Add(zid)
		return
	}
	zi.inverseRefs[key] = idset.New(zid)
	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 }

// GetDeadRefs returns all dead references as a sorted list.
func (zi *ZettelIndex) GetDeadRefs() *idset.Set { return zi.deadrefs }
func (zi *ZettelIndex) GetDeadRefs() *id.Set { return zi.deadrefs }

// GetMeta return just the raw metadata.
func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta }

// GetBackRefs returns all back references as a sorted list.
func (zi *ZettelIndex) GetBackRefs() *idset.Set { return zi.backrefs }
func (zi *ZettelIndex) GetBackRefs() *id.Set { return zi.backrefs }

// GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetInverseRefs() map[string]*idset.Set {
func (zi *ZettelIndex) GetInverseRefs() map[string]*id.Set {
	if len(zi.inverseRefs) == 0 {
		return nil
	}
	result := make(map[string]*idset.Set, len(zi.inverseRefs))
	result := make(map[string]*id.Set, len(zi.inverseRefs))
	for key, refs := range zi.inverseRefs {
		result[key] = refs
	}
	return result
}

// GetWords returns a reference to the set of words. It must not be modified.
func (zi *ZettelIndex) GetWords() WordSet { return zi.words }

// GetUrls returns a reference to the set of URLs. It must not be modified.
func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls }

Changes to box/membox/membox.go.

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

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

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







-






+







package membox

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

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

func init() {
	manager.Register(
		"mem",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &memBox{
92
93
94
95
96
97
98
99

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

99
100
101
102
103
104
105
106







-
+







	mb.mx.RLock()
	defer mb.mx.RUnlock()
	return len(mb.zettel) < mb.maxZettel
}

func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) {
	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.ByteSize()
	newBytes := mb.curBytes + zettel.Length()
	if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return id.Invalid, box.ErrCapacity
	}
	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
		_, ok := mb.zettel[zid]
		return !ok, nil
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
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







-
+

-
+











-
+

-
+







	mb.mx.RLock()
	defer mb.mx.RUnlock()
	zid := zettel.Meta.Zid
	if !zid.IsValid() {
		return false
	}

	newBytes := mb.curBytes + zettel.ByteSize()
	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[zid]; found {
		newBytes -= prevZettel.ByteSize()
		newBytes -= prevZettel.Length()
	}
	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.ByteSize()
	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[m.Zid]; found {
		newBytes -= prevZettel.ByteSize()
		newBytes -= prevZettel.Length()
	}
	if mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return box.ErrCapacity
	}

	zettel.Meta = m
217
218
219
220
221
222
223
224

225
226
227
228
229
230
231
232
233
234
235
236
237
217
218
219
220
221
222
223

224
225
226
227
228
229
230
231
232
233
234
235
236
237







-
+













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

func (mb *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mb.mx.RLock()
	st.Zettel = len(mb.zettel)
	mb.mx.RUnlock()
	mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

Changes to box/notify/directory.go.

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


30
31
32
33
34
35
36
16
17
18
19
20
21
22


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







-
-





+
+







import (
	"errors"
	"fmt"
	"path/filepath"
	"regexp"
	"sync"

	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/box"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type entrySet map[id.Zid]*DirEntry

// DirServiceState signal the internal state of the service.
//
// The following state transitions are possible:
272
273
274
275
276
277
278
279
280


281
282
283
284
285
286
287

288
289
290
291
292
293
294
272
273
274
275
276
277
278


279
280
281
282
283
284
285
286

287
288
289
290
291
292
293
294







-
-
+
+






-
+







		}
	default:
		ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
	}
	return newEntries, true
}

func getNewZids(entries entrySet) []id.Zid {
	zids := make([]id.Zid, 0, len(entries))
func getNewZids(entries entrySet) id.Slice {
	zids := make(id.Slice, 0, len(entries))
	for zid := range entries {
		zids = append(zids, zid)
	}
	return zids
}

func (ds *DirService) onCreateDirectory(zids []id.Zid, prevEntries entrySet) {
func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) {
	for _, zid := range zids {
		ds.notifyChange(zid, box.OnZettel)
		delete(prevEntries, zid)
	}

	// These were previously stored, by are not found now.
	// Notify system that these were deleted, e.g. for updating the index.
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
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







-
+



-
-
+
+


-
+




-
-
+
+




















-
+







func newNameIsBetter(oldName, newName string) bool {
	if len(oldName) < len(newName) {
		return false
	}
	return oldName > newName
}

var supportedSyntax, primarySyntax *set.Set[string]
var supportedSyntax, primarySyntax strfun.Set

func init() {
	syntaxList := parser.GetSyntaxes()
	supportedSyntax = set.New(syntaxList...)
	primarySyntax = set.New[string]()
	supportedSyntax = strfun.NewSet(syntaxList...)
	primarySyntax = make(map[string]struct{}, len(syntaxList))
	for _, syntax := range syntaxList {
		if parser.Get(syntax).Name == syntax {
			primarySyntax.Add(syntax)
			primarySyntax.Set(syntax)
		}
	}
}
func newExtIsBetter(oldExt, newExt string) bool {
	oldSyntax := supportedSyntax.Contains(oldExt)
	if oldSyntax != supportedSyntax.Contains(newExt) {
	oldSyntax := supportedSyntax.Has(oldExt)
	if oldSyntax != supportedSyntax.Has(newExt) {
		return !oldSyntax
	}
	if oldSyntax {
		if oldExt == "zmk" {
			return false
		}
		if newExt == "zmk" {
			return true
		}
		oldInfo := parser.Get(oldExt)
		newInfo := parser.Get(newExt)
		if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser {
			return !oldASTParser
		}
		if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat {
			return !oldTextFormat
		}
		if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat {
			return oldImageFormat
		}
		if oldPrimary := primarySyntax.Contains(oldExt); oldPrimary != primarySyntax.Contains(newExt) {
		if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) {
			return !oldPrimary
		}
	}

	oldLen := len(oldExt)
	newLen := len(newExt)
	if oldLen != newLen {

Changes to box/notify/directory_test.go.

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


27
28
29
30
31
32
33
12
13
14
15
16
17
18


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







-
-






+
+







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

package notify

import (
	"testing"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func TestSeekZid(t *testing.T) {
	testcases := []struct {
		name string
		zid  id.Zid
	}{
48
49
50
51
52
53
54
55

56
57
58


59
60
61


62
63

64
65
66
67
68
69
70
48
49
50
51
52
53
54

55
56


57
58
59


60
61
62

63
64
65
66
67
68
69
70







-
+

-
-
+
+

-
-
+
+

-
+







		}
	}
}

func TestNewExtIsBetter(t *testing.T) {
	extVals := []string{
		// Main Formats
		meta.ValueSyntaxZmk, meta.ValueSyntaxDraw, meta.ValueSyntaxMarkdown, meta.ValueSyntaxMD,
		meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD,
		// Other supported text formats
		meta.ValueSyntaxCSS, meta.ValueSyntaxSxn, meta.ValueSyntaxTxt, meta.ValueSyntaxHTML,
		meta.ValueSyntaxText, meta.ValueSyntaxPlain,
		meta.SyntaxCSS, meta.SyntaxSxn, meta.SyntaxTxt, meta.SyntaxHTML,
		meta.SyntaxText, meta.SyntaxPlain,
		// Supported text graphics formats
		meta.ValueSyntaxSVG,
		meta.ValueSyntaxNone,
		meta.SyntaxSVG,
		meta.SyntaxNone,
		// Supported binary graphic formats
		meta.ValueSyntaxGif, meta.ValueSyntaxPNG, meta.ValueSyntaxJPEG, meta.ValueSyntaxWebp, meta.ValueSyntaxJPG,
		meta.SyntaxGif, meta.SyntaxPNG, meta.SyntaxJPEG, meta.SyntaxWebp, meta.SyntaxJPG,

		// Unsupported syntax values
		"gz", "cpp", "tar", "cppc",
	}
	for oldI, oldExt := range extVals {
		for newI, newExt := range extVals {
			if oldI <= newI {

Changes to box/notify/entry.go.

12
13
14
15
16
17
18
19

20
21
22


23
24
25
26
27
28
29
12
13
14
15
16
17
18

19

20
21
22
23
24
25
26
27
28
29
30







-
+
-


+
+







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

package notify

import (
	"path/filepath"

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

const (
	extZettel = "zettel" // file contains metadata and content
	extBin    = "bin"    // file contains binary content
	extTxt    = "txt"    // file contains non-binary content
)
46
47
48
49
50
51
52
53

54
55
56
57
58
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
47
48
49
50
51
52
53

54
55
56
57
58
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







-
+










-
+




















-
-
+
+


-
+






-
+


-
+




-
+







-
+











// HasMetaInContent returns true, if metadata will be stored in the content file.
func (e *DirEntry) HasMetaInContent() bool {
	return e.IsValid() && extIsMetaAndContent(e.ContentExt)
}

// SetupFromMetaContent fills entry data based on metadata and zettel content.
func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []meta.Value) {
func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []string) {
	if e.Zid != m.Zid {
		panic("Zid differ")
	}
	if contentName := e.ContentName; contentName != "" {
		if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" {
			e.MetaName = e.calcBaseName(contentName)
		}
		return
	}

	syntax := m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)
	syntax := m.GetDefault(api.KeySyntax, meta.DefaultSyntax)
	ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax)
	metaName := e.MetaName
	eimc := extIsMetaAndContent(ext)
	if eimc {
		if metaName != "" {
			ext = contentExtWithMeta(syntax, content)
		}
		e.ContentName = e.calcBaseName(metaName) + "." + ext
		e.ContentExt = ext
	} else {
		if len(content.AsBytes()) > 0 {
			e.ContentName = e.calcBaseName(metaName) + "." + ext
			e.ContentExt = ext
		}
		if metaName == "" {
			e.MetaName = e.calcBaseName(e.ContentName)
		}
	}
}

func contentExtWithMeta(syntax meta.Value, content zettel.Content) string {
	p := parser.Get(string(syntax))
func contentExtWithMeta(syntax string, content zettel.Content) string {
	p := parser.Get(syntax)
	if content.IsBinary() {
		if p.IsImageFormat {
			return string(syntax)
			return syntax
		}
		return extBin
	}
	if p.IsImageFormat {
		return extTxt
	}
	return string(syntax)
	return syntax
}

func calcContentExt(syntax meta.Value, yamlSep bool, getZettelFileSyntax func() []meta.Value) string {
func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string {
	if yamlSep {
		return extZettel
	}
	switch syntax {
	case meta.ValueSyntaxNone, meta.ValueSyntaxZmk:
	case meta.SyntaxNone, meta.SyntaxZmk:
		return extZettel
	}
	for _, s := range getZettelFileSyntax() {
		if s == syntax {
			return extZettel
		}
	}
	return string(syntax)
	return syntax

}

func (e *DirEntry) calcBaseName(name string) string {
	if name == "" {
		return e.Zid.String()
	}
	return name[0 : len(name)-len(filepath.Ext(name))]

}

Changes to cmd/cmd_file.go.

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


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

47
48
49

50
51
52
53
54
55
56

57
58
59
60
61
62
63
17
18
19
20
21
22
23


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

46
47
48

49


50
51
52
53

54
55
56
57
58
59
60
61







-
-




+
+
















-
+


-
+
-
-




-
+







	"context"
	"flag"
	"fmt"
	"io"
	"os"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

	return 0, nil
}

Changes to cmd/cmd_password.go.

16
17
18
19
20
21
22
23
24
25



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



23
24
25
26
27
28
29
30
31
32







-
-
-
+
+
+







import (
	"flag"
	"fmt"
	"os"

	"golang.org/x/term"

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

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

func cmdPassword(fs *flag.FlagSet) (int, error) {
	if fs.NArg() == 0 {
		fmt.Fprintln(os.Stderr, "User name and user zettel identification missing")
59
60
61
62
63
64
65
66
67


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


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







-
-
+
+











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

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

Changes to cmd/cmd_run.go.

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

30
31
32
33
34
35
36
14
15
16
17
18
19
20

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







-








+







package cmd

import (
	"context"
	"flag"
	"net/http"

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

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

func flgRun(fs *flag.FlagSet) {
	fs.String("c", "", "configuration file")
	fs.Uint("a", 0, "port number kernel service (0=disable)")

Changes to cmd/command.go.

11
12
13
14
15
16
17
18
19
20

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


18
19
20
21
22
23
24
25
26







-
-

+







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

package cmd

import (
	"flag"
	"maps"
	"slices"

	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/logger"
)

// Command stores information about commands / sub-commands.
type Command struct {
	Name       string              // command name as it appears on the command line
	Func       CommandFunc         // function that executes a command
63
64
65
66
67
68
69
70

62
63
64
65
66
67
68

69







-
+
// Get returns the command identified by the given name and a bool to signal success.
func Get(name string) (Command, bool) {
	cmd, ok := commands[name]
	return cmd, ok
}

// List returns a sorted list of all registered command names.
func List() []string { return slices.Sorted(maps.Keys(commands)) }
func List() []string { return maps.Keys(commands) }

Changes to cmd/main.go.

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


41
42
43
44
45
46
47
22
23
24
25
26
27
28


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







-
-










+
+







	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/impl"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/compbox"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const strRunSimple = "run-simple"

func init() {
	RegisterCommand(Command{
		Name: "help",
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
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







-
+

-
+








-
+

-
+

-
+

-
+

-
+






-
-
+
+







}

func getConfig(fs *flag.FlagSet) (string, *meta.Meta) {
	filename, cfg := fetchStartupConfiguration(fs)
	fs.Visit(func(flg *flag.Flag) {
		switch flg.Name {
		case "p":
			cfg.Set(keyListenAddr, meta.Value(net.JoinHostPort("127.0.0.1", flg.Value.String())))
			cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String()))
		case "a":
			cfg.Set(keyAdminPort, meta.Value(flg.Value.String()))
			cfg.Set(keyAdminPort, flg.Value.String())
		case "d":
			val := flg.Value.String()
			if strings.HasPrefix(val, "/") {
				val = "dir://" + val
			} else {
				val = "dir:" + val
			}
			deleteConfiguredBoxes(cfg)
			cfg.Set(keyBoxOneURI, meta.Value(val))
			cfg.Set(keyBoxOneURI, val)
		case "l":
			cfg.Set(keyLogLevel, meta.Value(flg.Value.String()))
			cfg.Set(keyLogLevel, flg.Value.String())
		case "debug":
			cfg.Set(keyDebug, meta.Value(flg.Value.String()))
			cfg.Set(keyDebug, flg.Value.String())
		case "r":
			cfg.Set(keyReadOnly, meta.Value(flg.Value.String()))
			cfg.Set(keyReadOnly, flg.Value.String())
		case "v":
			cfg.Set(keyVerbose, meta.Value(flg.Value.String()))
			cfg.Set(keyVerbose, flg.Value.String())
		}
	})
	return filename, cfg
}

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

const (
	keyAdminPort         = "admin-port"
182
183
184
185
186
187
188
189

190
191
192
193
194
195
196
182
183
184
185
186
187
188

189
190
191
192
193
194
195
196







-
+








func setServiceConfig(cfg *meta.Meta) bool {
	debugMode := cfg.GetBool(keyDebug)
	if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel {
		kernel.Main.SetLogLevel(logger.DebugLevel.String())
	}
	if logLevel, found := cfg.Get(keyLogLevel); found {
		kernel.Main.SetLogLevel(string(logLevel))
		kernel.Main.SetLogLevel(logLevel)
	}
	err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode)
	err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
	if val, found := cfg.Get(keyAdminPort); found {
		err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val)
	}

277
278
279
280
281
282
283
284

285
286
287
288

289
290
291
292
293
294
295
277
278
279
280
281
282
283

284
285
286
287

288
289
290
291
292
293
294
295







-
+



-
+








	secret := cfg.GetDefault("secret", "")
	if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" {
		fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret)
		return 2
	}
	cfg.Delete("secret")
	secretHash := fmt.Sprintf("%x", sha256.Sum256([]byte(string(secret))))
	secret = fmt.Sprintf("%x", sha256.Sum256([]byte(secret)))

	kern.SetCreators(
		func(readonly bool, owner id.Zid) (auth.Manager, error) {
			return impl.New(readonly, owner, secretHash), nil
			return impl.New(readonly, owner, secret), nil
		},
		createManager,
		func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error {
			setupRouting(srv, plMgr, authMgr, rtConfig)
			return nil
		},
	)

Changes to collect/order.go.

12
13
14
15
16
17
18
19
20


21
22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
38

39
40
41
42


43
44
45
46
47
48
49

50
51
52

53



54
55

56
57

58
59

60
61
62
63
64

65
66
67
68
69
70
71
12
13
14
15
16
17
18


19
20
21
22
23
24
25
26
27
28


29
30
31
32
33
34
35
36
37

38
39
40


41
42
43
44
45
46
47
48

49
50
51
52
53

54
55
56
57

58
59

60
61

62
63
64
65
66

67
68
69
70
71
72
73
74







-
-
+
+








-
-
+
+







-
+


-
-
+
+






-
+



+
-
+
+
+

-
+

-
+

-
+




-
+







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

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

// Order of internal links within the given zettel.
func Order(zn *ast.ZettelNode) (result []*ast.LinkNode) {
// Order of internal reference within the given zettel.
func Order(zn *ast.ZettelNode) (result []*ast.Reference) {
	for _, bn := range zn.Ast {
		ln, ok := bn.(*ast.NestedListNode)
		if !ok {
			continue
		}
		switch ln.Kind {
		case ast.NestedListOrdered, ast.NestedListUnordered:
			for _, is := range ln.Items {
				if ln := firstItemZettelLink(is); ln != nil {
					result = append(result, ln)
				if ref := firstItemZettelReference(is); ref != nil {
					result = append(result, ref)
				}
			}
		}
	}
	return result
}

func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode {
func firstItemZettelReference(is ast.ItemSlice) *ast.Reference {
	for _, in := range is {
		if pn, ok := in.(*ast.ParaNode); ok {
			if ln := firstInlineZettelLink(pn.Inlines); ln != nil {
				return ln
			if ref := firstInlineZettelReference(pn.Inlines); ref != nil {
				return ref
			}
		}
	}
	return nil
}

func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) {
func firstInlineZettelReference(is ast.InlineSlice) (result *ast.Reference) {
	for _, inl := range is {
		switch in := inl.(type) {
		case *ast.LinkNode:
			if ref := in.Ref; ref.IsZettel() {
			return in
				return ref
			}
			result = firstInlineZettelReference(in.Inlines)
		case *ast.EmbedRefNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelReference(in.Inlines)
		case *ast.EmbedBLOBNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelReference(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelReference(in.Inlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelReference(in.Inlines)
		default:
			continue
		}
		if result != nil {
			return result
		}
	}

Changes to config/config.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
13
14
15
16
17
18
19

20
21
22
23
24


25
26




27
28
29


30
31
32
33
34
35
36
37







-
+




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








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

import (
	"context"

	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

// Key values that are supported by Config.Get
const (
	KeyFooterZettel         = "footer-zettel"
	KeyHomeZettel           = "home-zettel"
	KeyFooterZettel       = "footer-zettel"
	KeyHomeZettel         = "home-zettel"
	KeyListsMenuZettel      = "lists-menu-zettel"
	KeyShowBackLinks        = "show-back-links"
	KeyShowFolgeLinks       = "show-folge-links"
	KeyShowSequelLinks      = "show-sequel-links"
	KeyShowBackLinks      = "show-back-links"
	KeyShowFolgeLinks     = "show-folge-links"
	KeyShowSequelLinks    = "show-sequel-links"
	KeyShowSubordinateLinks = "show-subordinate-links"
	KeyShowSuccessorLinks   = "show-successor-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

53
54
55
56
57
58
59
60

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

58
59
60
61
62
63
64
65







-
+







	// GetMaxTransclusions returns the maximum number of indirect transclusions.
	GetMaxTransclusions() int

	// GetYAMLHeader returns the current value of the "yaml-header" key.
	GetYAMLHeader() bool

	// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
	GetZettelFileSyntax() []meta.Value
	GetZettelFileSyntax() []string
}

// AuthConfig are relevant configuration values for authentication.
type AuthConfig interface {
	// GetSimpleMode returns true if system tuns in simple-mode.
	GetSimpleMode() bool

96
97
98
99
100
101
102
103

104
105

106
107
108
109


110
111
112
94
95
96
97
98
99
100

101
102

103

104


105
106
107
108
109







-
+

-
+
-

-
-
+
+



	return "secure"
}

// AllowHTML returns true, if the given HTML insecurity level matches the given syntax value.
func (hi HTMLInsecurity) AllowHTML(syntax string) bool {
	switch hi {
	case SyntaxHTML:
		return syntax == meta.ValueSyntaxHTML
		return syntax == meta.SyntaxHTML
	case MarkdownHTML:
		return syntax == meta.ValueSyntaxHTML || syntax == meta.ValueSyntaxMarkdown ||
		return syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD
			syntax == meta.ValueSyntaxMD
	case ZettelmarkupHTML:
		return syntax == meta.ValueSyntaxZmk || syntax == meta.ValueSyntaxHTML ||
			syntax == meta.ValueSyntaxMarkdown || syntax == meta.ValueSyntaxMD
		return syntax == meta.SyntaxZmk || syntax == meta.SyntaxHTML ||
			syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD
	}
	return false
}

Changes to docs/manual/00001001000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16

17
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15

16
17






-
+








-
+

id: 00001001000000
title: Introduction to the Zettelstore
role: manual
tags: #introduction #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102181246
modified: 20240710184612

[[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] involves collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity.
It's done by most individuals, not necessarily as part of their main business.
It's essential for knowledge workers, such as students, researchers, lecturers, software developers, scientists, engineers, architects, etc.
Many hobbyists build up a significant amount of knowledge, even if they do not need to think for a living.
Personal knowledge management can be seen as a prerequisite for many kinds of collaboration.

Zettelstore is software that collects and relates your notes (""zettel"") to represent and enhance your knowledge, supporting the ""[[Zettelkasten method|https://en.wikipedia.org/wiki/Zettelkasten]]"".
The method is based on creating many individual notes, each containing one idea or piece of information, which are related to each other.
The method is based on creating many individual notes, each with one idea or piece of information, that is related to each other.
Since knowledge is typically built up gradually, one major focus is a long-term store of these notes, hence the name ""Zettelstore"".

Changes to docs/manual/00001002000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16

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

32
33
34

35
36
37
38
39
40

41
42
43
1
2
3
4
5
6

7
8
9
10
11
12

13
14
15

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

31
32
33

34
35
36
37
38
39

40
41
42
43






-
+





-
+


-
+














-
+


-
+





-
+



id: 00001002000000
title: Design goals for the Zettelstore
role: manual
tags: #design #goal #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102191434
modified: 20230624171152

Zettelstore supports the following design goals:

; Longevity of stored notes / zettel
: Every zettel you create should be readable without the help of any tool, even without Zettelstore.
: It should not hard to write other software that works with your zettel.
: It should be not hard to write other software that works with your zettel.
: Normal zettel should be stored in a single file.
  If this is not possible: at most in two files: one for the metadata, one for the content.
  The only exceptions are [[predefined zettel|00001005090000]] stored in the Zettelstore executable.
  The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable.
: There is no additional database.
; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.
  If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel.
: If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel.
; Ease of installation
: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working.
: Upgrading the software is done just by replacing the executable with a newer one.
; Ease of operation
: There is only one executable for Zettelstore and one directory, where your zettel are stored.
: If you decide to use multiple directories, you are free to configure Zettelstore appropriately.
; Multiple modes of operation
: You can use Zettelstore as a standalone software on your device, but you are not restricted to it.
: You can install the software on a central server, or you can install it on all your devices with no restrictions on how to synchronize your zettel.
: You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel.
; Multiple user interfaces
: Zettelstore provides a default [[web-based user interface|00001014000000]].
  Anyone can provide alternative user interfaces, e.g. for special purposes.
  Anybody can provide alternative user interfaces, e.g. for special purposes.
; Simple service
: The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them.
: External software can be written to deeply analyze your zettel and the structures they form.
; Security by default
: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks.
: If you know what you are doing, Zettelstore allows you to relax some security-related preferences.
: If you know what use are doing, Zettelstore allows you to relax some security-related preferences.
  However, even in this case, the more secure way is chosen.
: The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed.
: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software.

Changes to docs/manual/00001003000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12

13
14

15
16
17
18

19
20
21
22
23
24

25
26
27
28
29
30
31
1
2
3
4
5
6

7
8
9
10
11

12
13

14
15
16
17

18
19
20
21
22
23

24
25
26
27
28
29
30
31






-
+




-
+

-
+



-
+





-
+







id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102185359
modified: 20241213101917

=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it to any directory
* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software.
  If you encounter a problem, please refer to the [[Troubleshooting|00001018000000]] page.]
  If you encounter a problem, please take a look on the [[Troubleshooting|00001018000000]] page.]
* A sub-directory ""zettel"" will be created in the directory where you put the executable.
  It will contain your future zettel.
* Open the URI [[http://localhost:23123]] with your web browser.
  A mostly empty Zettelstore is presented.
  It will present you a mostly empty Zettelstore.
  There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information.
* Please read the instructions for the [[web-based user interface|00001014000000]] and learn about the various ways to write zettel.
* If you restart your device, please make sure to start your Zettelstore again.

=== The intermediate user
You have already tried the Zettelstore software and now you want to use it permanently.
You already tried the Zettelstore software and now you want to use it permanently.
Zettelstore should start automatically when you log into your computer.

Please follow [[these instructions|00001003300000]].

=== The server administrator
You want to provide a shared Zettelstore that can be used from your various devices.
Installing Zettelstore as a Linux service is not that hard.

Changes to docs/manual/00001003300000.zettel.

1
2
3
4
5
6
7

8
9

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

23
24
25
26
27
1
2
3
4
5
6

7
8

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

22
23
24
25
26
27






-
+

-
+












-
+





id: 00001003300000
title: Zettelstore installation for the intermediate user
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20250102190221
modified: 20220114175754

You have already tried the Zettelstore software and now you want to use it permanently.
You already tried the Zettelstore software and now you want to use it permanently.
Zettelstore should start automatically when you log into your computer.

* Grab the appropriate executable and copy it into the appropriate directory
* If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]].
* If you created a startup configuration file, you need to test it:
** Start a command line prompt for your operating system.
** Navigate to the directory, where you placed the Zettelstore executable.
   In most cases, this is done by the command ``cd DIR``, where ''DIR'' denotes the directory, where you placed the executable.
** Start the Zettelstore:
*** On Windows execute the command ``zettelstore.exe run -c CONFIG_FILE``
*** On macOS execute the command ``./zettelstore run -c CONFIG_FILE``
*** On Linux execute the command ``./zettelstore run -c CONFIG_FILE``
** In all cases ''CONFIG_FILE'' must be replaced with the file name where you wrote the startup configuration.
** In all cases ''CONFIG_FILE'' must be substituted by file name where you wrote the startup configuration.
** If you encounter some error messages, update the startup configuration, and try again.
* Depending on your operating system, there are different ways to register Zettelstore to start automatically:
** [[Windows|00001003305000]]
** [[macOS|00001003310000]]
** [[Linux|00001003315000]]

Changes to docs/manual/00001003315000.zettel.

1
2
3
4
5
6
7

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

23
24
25
26
27
28
29
1
2
3
4
5
6

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

22
23
24
25
26
27
28
29






-
+














-
+







id: 00001003315000
title: Enable Zettelstore to start automatically on Linux
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20220114181521
modified: 20250102221716
modified: 20220307104944

Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore.

* One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]].
** See below for a lighter alternative.
* If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool"").
  It allows to specify application that should run on startup / login.
* [[KDE|https://kde.org/]] provides a system setting to [[autostart|https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/autostart/]] applications.
* [[Xfce|https://xfce.org/]] allows to specify [[autostart applications|https://docs.xfce.org/xfce/xfce4-session/preferences#application_autostart]].
* [[LXDE|https://www.lxde.org/]] uses [[LXSession Edit|https://wiki.lxde.org/en/LXSession_Edit]] to allow users to specify autostart applications.

If you use a different desktop environment, it often helps to to provide its name and the string ""autostart"" to google for it with the search engine of your choice.

Yet another way is to make use of the middleware that is provided.
Many Linux distributions make use of [[systemd|https://systemd.io/]], which allows to start processes on behalf of a user.
Many Linux distributions make use of [[systemd|https://systemd.io/]], which allows to start processes on behalf of an user.
On the command line, adapt the following script to your own needs and execute it:
```
# mkdir -p "$HOME/.config/systemd/user"
# cd "$HOME/.config/systemd/user"
# cat <<__EOF__ > zettelstore.service
[Unit]
Description=Zettelstore

Changes to docs/manual/00001004000000.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001004000000
title: Configuration of Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102181034
modified: 20210510153233

There are several levels to change the behavior and/or the appearance of Zettelstore.
There are some levels to change the behavior and/or the appearance of Zettelstore.

# The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface).
#* [[Command line parameters|00001004050000]]
# As an intermediate user, you usually want to have more control over how Zettelstore is started.
  This may include the URI under which your Zettelstore is accessible, or the directories in which your Zettel are stored.
  You may want to permanently store the command line parameters so that you don't have to specify them every time you start Zettelstore.
#* [[Zettelstore startup configuration|00001004010000]]

Changes to docs/manual/00001004010000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







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

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

22
23
24
25
26
27
28
29

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

29
30
31
32
33
34
35
36







-
+







  A value of ""0"" (the default) disables it.
  The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]].

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

  Default: ""0""
; [!asset-dir|''asset-dir'']
: Allows to specify a directory whose files are allowed to be transferred directly with the help of the web server.
: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server.
  The URL prefix for these files is ''/assets/''.
  You can use this if you want to transfer files that are too large for a zettel, such as presentation, PDF, music or video files.

  Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the very special case that the directory is one of the configured [[boxes|#box-uri-x]].]

  If you specify only the URL prefix in your web client, the contents of the directory are listed.
  To avoid this, create an empty file in the directory named ""index.html"".

Changes to docs/manual/00001004011200.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16
17
18
19
20
1
2
3
4
5
6

7
8
9
10
11
12

13
14
15
16
17
18
19
20






-
+





-
+







id: 00001004011200
title: Zettelstore boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102185551
modified: 20220307121547

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

An example is the [[predefined zettel|00001005090000]] that come with a Zettelstore.
An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore.
They are stored within the software itself.
In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting.

To cope with these (and more) situations, you configure Zettelstore to use one or more __boxes__.
This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number).
Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses.

Changes to docs/manual/00001004011400.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23






-
+








-
+







id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102180416
modified: 20240710180215

Under certain circumstances, it is preferable to further configure a file directory box.
This is done by appending query parameters after the base box URI ''dir:\//DIR''.

The following parameters are supported:

|= Parameter:|Description|Default value:|
|type|(Sub-) Type of the directory service|(value of ""[[default-dir-box-type|00001004010000#default-dir-box-type]]"")
|worker|Number of workers that can access the directory in parallel|7
|worker|Number of worker that can access the directory in parallel|7
|readonly|Allow only operations that do not create or change zettel|n/a

=== Type
On some operating systems, Zettelstore tries to detect changes to zettel files outside of Zettelstore's control[^This includes Linux, Windows, and macOS.].
On other operating systems, this may be not possible, due to technical limitations.
Automatic detection of external changes is also not possible, if zettel files are put on an external service, such as a file server accessed via SMB/CIFS or NFS.

Changes to docs/manual/00001004011600.zettel.

1
2
3
4
5
6
7

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

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

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

20
21
22
23
24
25
26






-
+












-
+






id: 00001004011600
title: Configure memory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20220307112918
modified: 20250102222236
modified: 20220307122554

Under most circumstances, it is preferable to further configure a memory box.
This is done by appending query parameters after the base box URI ''mem:''.

The following parameters are supported:

|= Parameter:|Description|Default value:|Maximum value:
|max-bytes|Maximum number of bytes the box will store|65535|1073741824 (1 GiB)
|max-zettel|Maximum number of zettel|127|65535

The default values are somehow arbitrarily, but applicable for many use cases.

While the number of zettel should be easily calculable by a user, the number of bytes might be a little more difficult.
While the number of zettel should be easily calculable by an user, the number of bytes might be a little more difficult.

Metadata consumes 6 bytes for the zettel identifier and for each metadata value one byte for the separator, plus the length of key and data.
Then size of the content is its size in bytes.
For text content, its the number of bytes for its UTF-8 encoding.

If one of the limits are exceeded, Zettelstore will give an error indication, based on the HTTP status code 507.

Changes to docs/manual/00001004020000.zettel.

1
2

3
4
5
6
7

8
9
10
11
12
13
14
1

2
3
4
5
6

7
8
9
10
11
12
13
14

-
+




-
+







id: 00001004020000
title: Configure a running Zettelstore
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250131151530
modified: 20241118175216
show-back-links: false

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

45
46
47

48
49
50
51
52
53

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

72
73

74
75
76
77
78
79
80
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51
52
53

54
55
56
57









58
59
60
61
62

63
64

65
66
67
68
69
70
71
72







-
+



+





-
+



-
-
-
-
-
-
-
-
-





-
+

-
+







  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.
: Specifies the identifier of the zettel, that should be presented for the default view / home view.
  If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown.

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

; [!lang|''lang'']
: Language to be used when displaying content.

  Default: ""en"".

  This value is used as a default value, if it is not set in a user's zettel or in a zettel.
  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]].
; [!lists-menu-zettel|''lists-menu-zettel'']
: Identifier of the zettel that specifies entries of the ""Lists"" menu (in the [[Web user interface|00001014000000]]).
  Every list item with a [[link|00001007040310]] is translated into a menu entry.

  If not given or if the identifier does not identify a zettel, or the zettel is not accessible for the current user, the zettel with the identifier ''00000000080001'' is used.

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

  Default: ""00000000080001"".
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

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

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

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

  Default: """".

Changes to docs/manual/00001004020200.zettel.

1
2
3
4
5
6
7

8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24
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: 20250131151259
modified: 20241118175124

Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]].
A subset of those may be overwritten in the zettel that is currently used.
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
|[[''lists-menu-zettel''|00001004020000#lists-menu-zettel]]|Y|N|
|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y|
|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y|
|[[''show-sequel-links''|00001004020000#show-sequel-links]]|Y|Y|
|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y|
|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y|

Changes to docs/manual/00001004050000.zettel.

1
2
3
4
5
6
7

8
9
10

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

23
24
25
26
27

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

7
8
9

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

22
23
24
25
26

27
28
29
30
31
32
33
34






-
+


-
+











-
+




-
+







id: 00001004050000
title: Command line parameters
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102174436
modified: 20221128161932

Zettelstore is not just a service that provides services of a zettelkasten.
It allows some tasks to be executed at the command line.
It allows to some tasks to be executed at the command line.
Typically, the task (""sub-command"") will be given at the command line as the first parameter.

If no parameter is given, the Zettelstore is called as
```
zettelstore
```
This is equivalent to call it this way:
```sh
mkdir -p ./zettel
zettelstore run -d ./zettel -c ./.zscfg
```
Typically this is done by starting Zettelstore via a graphical user interface by double-clicking its file icon.
Typically this is done by starting Zettelstore via a graphical user interface by double-clicking to its file icon.
=== Sub-commands
* [[``zettelstore help``|00001004050200]] lists all available sub-commands.
* [[``zettelstore version``|00001004050400]] to display version information of Zettelstore.
* [[``zettelstore run``|00001004051000]] to start the Zettelstore service.
* [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by double-clicking in your GUI.
* [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI.
* [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services.
* [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]].

Every sub-command allows the following command line options:
; [!h|''-h''] (or ''--help'')
: Does not execute the sub-command, but shows allowed command line options (except ''-h'' / ''--help'').
; [!l|''-l LOGSPEC'']

Changes to docs/manual/00001004051100.zettel.

1
2
3
4
5
6
7

8
9
10
11


12
13
14
15
16
17
18
19

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

7
8
9


10
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26






-
+


-
-
+
+







-
+







id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102221633
modified: 20221128161922

=== ``zettelstore run-simple``
This sub-command is implicitly called, when a user starts Zettelstore by double-clicking on its GUI icon.
It is a simplified variant of the [[''run'' sub-command|00001004051000]].
This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon.
It is s simplified variant of the [[''run'' sub-command|00001004051000]].

First, this sub-command checks if it can read a [[Zettelstore startup configuration|00001004010000]] file by trying the [[default values|00001004051000#c]].
If this is the case, ''run-simple'' just continues as the [[''run'' sub-command|00001004051000]], but ignores any command line options (including ''-d DIR'').[^This allows a [[curious user|00001003000000]] to become an intermediate user.]


If no startup configuration was found, the sub-command allows only to specify a zettel directory.
The directory will be created automatically, if it does not exist.
This is a difference to the ''run'' sub-command, where the directory must exist.
This is a difference to the ''run'' sub-command, where the directory must exists.
In contrast to the ''run'' sub-command, other command line parameter are not allowed.

```
zettelstore run-simple [-d DIR]
```

; [!d|''-d DIR'']

Changes to docs/manual/00001004051400.zettel.

1
2
3
4
5
6
7

8
9

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

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

39
1
2
3
4
5
6

7
8

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

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

38
39






-
+

-
+











-
+
















-
+

id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102221851
modified: 20210712234305

This sub-command is used to create a hashed password for users to be authenticated.
This sub-command is used to create a hashed password for to be authenticated users.

It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output.

The general usage is:
```
zettelstore password IDENT ZETTEL-ID
```

``IDENT`` is the identification for the user that should be authenticated.
``ZETTEL-ID`` is the [[identifier of the zettel|00001006050000]] that later acts as a user zettel.

See [[Creating a user zettel|00001010040200]] for some background information.
See [[Creating an user zettel|00001010040200]] for some background information.

An example:

```
# zettelstore password bob 20200911115600
Password:
   Again:
credential: $2a$10$1q92v1Ya8Too5HD/4rKpPuCP8fZTYPochsC6DcY1T4JKwhSx8uLu6
user-id: bob
```

This will produce a hashed password (""credential"") for the new user ""bob"" to be stored in zettel ""20200911115600"".

You should copy the relevant output to the zettel of the user to be secured, especially by setting the meta keys ''credential'' and ''user-id'' to the copied values.

Please note that the generated hashed password is tied to the given user identification (''user-id'') and to the identifier of its zettel.
Changing one of these will prevent the user from being authenticated with the given password.
Changing one of those will stop authenticating the user with the given password.
In this case you have to re-run this sub-command.

Changes to docs/manual/00001004100000.zettel.

1
2
3
4
5
6
7

8
9

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

23
24
25
26
1
2
3
4
5
6

7
8

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

22
23
24
25
26






-
+

-
+












-
+




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

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

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

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

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

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

Changes to docs/manual/00001004101000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001004101000
title: List of supported commands of the administrator console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210510141304
modified: 20250102190201
modified: 20220823194553

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

  If a key ends with the hyphen-minus character (""''-''"", U+002D), the key denotes a list value.
36
37
38
39
40
41
42
43

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

43
44
45
46
47
48
49
50







-
+








  ``get-config`` shows all current configuration data.

  ``get-config SERVICE`` shows only the current configuration data of the given service.

  ``get-config SERVICE KEY`` shows the current configuration data for the given service and key.
; [!header|''header'']
: Toggles the header mode, where each table is shown with a header nor not.
: Toggles the header mode, where each table is show with a header nor not.
; [!log-level|''log-level'']
: Displays or sets the [[logging level|00001004059700]] for the kernel or a service.

  ``log-level`` shows all known log level.

  ``log-level NAME`` shows log level for the given service or for the kernel.

75
76
77
78
79
80
81
82

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

82
83
84
85
86
87
88
89







-
+







  It may be removed without any further notice at any time.
  In most cases, it is a tool for software developers to optimize Zettelstore's internal workings.
; [!refresh|''refresh'']
: Refresh all internal data about zettel.
; [!restart|''restart SERVICE'']
: Restart the given service and all other that depend on this.
; [!services|''services'']
: Displays a list of all available services and their current status.
: Displays s list of all available services and their current status.
; [!set-config|''set-config SERVICE KEY VALUE'']
: Sets a single configuration value for the next configuration of a given service.
  It will become effective if the service is restarted.

  If the key specifies a list value, all other list values with a number greater than the given key are deleted.
  You can use the special number ""0"" to delete all values.
  E.g. ``set-config box box-uri-0 any_text`` will remove all values of the list __box-uri-__.

Changes to docs/manual/00001005000000.zettel.

1
2
3
4
5
6
7

8
9
10
11


12
13
14
15

16
17
18
19
20
21
22
1
2
3
4
5
6

7
8
9


10
11
12
13
14

15
16
17
18
19
20
21
22






-
+


-
-
+
+



-
+







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

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

Zettelstore provides additional services to the user.
Via the built-in [[web user interface|00001014000000]] you can work with zettel in various ways.
Via the builtin [[web user interface|00001014000000]] you can work with zettel in various ways.
For example, you are able to list zettel, to create new zettel, to edit them, or to delete them.
You can view zettel details and relations between zettel.

In addition, Zettelstore provides an ""application programming interface"" ([[API|00001012000000]]) that allows other software to communicate with the Zettelstore.
Zettelstore becomes extensible by external software.
For example, a more sophisticated user interface could be built, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel.

37
38
39
40
41
42
43
44

45
46
47
48
49

50
51
52

53
54
55

56
57
58
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
37
38
39
40
41
42
43

44
45
46
47
48

49
50
51

52
53
54

55
56
57
58
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







-
+




-
+


-
+


-
+





-
+










-
+










-
+










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

Two filename extensions are used by Zettelstore:
# ''.zettel'' is a format that stores metadata and content together in one file,
# the empty file extension is used, when the content must be stored in its own file, e.g. image data;
  in this case, the filename contains just the 14 digits of the zettel identifier, and optional characters except the period ''"."''.  
  in this case, the filename just the 14 digits of the zettel identifier, and optional characters except the period ''"."''.  
Other filename extensions are used to determine the ""syntax"" of a zettel.
This allows to use other content within the Zettelstore, e.g. images or HTML templates.

For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file.
Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stored.
Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores.
The solution is a meta-file with the same zettel identifier, but without a filename extension.
Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure.
It maintains this relationship as long as these files exist.
It maintains this relationship as long as these files exists.

In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files.
Here the ''.zettel'' extension will signal that the metadata and the zettel content will be stored in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator"").
Here the ''.zettel'' extension will signal that the metadata and the zettel content will be put in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator"").

=== Predefined zettel

Zettelstore contains some [[predefined zettel|00001005090000]] to work properly.
The [[configuration zettel|00001004020000]] is one example.
To render the built-in [[web user interface|00001014000000]], some templates are used, as well as a [[layout specification in CSS|00000000020001]].
To render the builtin [[web user interface|00001014000000]], some templates are used, as well as a [[layout specification in CSS|00000000020001]].
The icon that visualizes a broken image is a [[predefined GIF image|00000000040001]].
All of these are visible to the Zettelstore as zettel.

One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences.

Where are these zettel stored?
They are stored within the Zettelstore software itself, because one [[design goal|00001002000000]] was to have just one executable file to use Zettelstore.
But data stored within an executable program cannot be changed later[^Well, it can, but it is a very bad idea to allow this. Mostly for security reasons.].

To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together.
If you change a zettel, it will be stored as a file.
If you change a zettel, it will be always stored as a file.
If a zettel is requested, Zettelstore will first try to read that zettel from a file.
If such a file was not found, the internal zettel store is searched secondly.

Therefore, the file store ""shadows"" the internal zettel store.
If you want to read the original zettel, you have to delete the zettel (which removes it from the file directory).
Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software.

* [[List of predefined zettel|00001005090000]]

=== Boxes: alternative ways to store zettel
As described above, a zettel may be stored either as a file inside a directory or within the Zettelstore software itself.
As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself.
Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.]

A file directory which stores zettel is called a ""directory box"".
But zettel may be also stored in a ZIP file, which is called ""file box"".
For testing purposes, zettel may be stored in volatile memory (called __RAM__).
This way is called ""memory box"".

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

Changes to docs/manual/00001005090000.zettel.

1
2
3
4
5
6
7

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

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

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

1
2
3
4
5
6

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

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

44
45
46

47
48
49
50
51
52
53
54
55
56
57

58






-
+

















-
+


















-
+


-











-
+
id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250131151733
modified: 20241202102358

The following table lists all predefined zettel with their purpose.

The content of most[^To be more exact: zettel with an identifier greater or equal ''00000999999900'' will have their content indexed.] of these zettel will not be indexed by Zettelstore.
You will not find zettel when searched for some content, e.g. ""[[query:european]]"" will not find the [[Zettelstore License|00000000000004]].
However, metadata is always indexed, e.g. ""[[query:title:license]]"" will find the Zettelstore License zettel.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore
| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore
| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content
| [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages
| [[00000000000008]] | Zettelstore Memory | Some statistics about main memory usage
| [[00000000000009]] | Zettelstore Sx Engine | Statistics about the [[Sx|https://t73f.de/r/sx]] engine, which interprets symbolic expressions
| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the index process
| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore
| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]
| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]]
| [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel
| [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel
| [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel
| [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text
| [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel
| [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message
| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates
| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid
| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]""
| [[00000000060020]] | configuration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#configuration]]""
| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]""
| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]""
| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]""
| [[00000000080001]] | Lists Menu | Default items of the ""Lists"" menu; see [[lists-menu-zettel|00001004020000#lists-menu-zettel]] for customization options
| [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]
| [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]]
| [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]]
| [[00000999999999]] | Zettelstore Application Directory | Maps application name to application specific zettel
| [[00010000000000]] | Home | Default home zettel, contains some welcome information

If a zettel is not linked, it is not accessible for the current user.
In most cases, you must at least enable [[''expert-mode''|00001004020000#expert-mode]].

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

Changes to docs/manual/00001006000000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001006000000
title: Layout of a Zettel
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102190828
modified: 20230403123541

A zettel consists of two parts: the metadata and the zettel content.
Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore.
The zettel content is, well, the actual content.
In many cases, the content is in plain text form.
Plain text is long-lasting.
However, content in binary format is also possible.
38
39
40
41
42
43
44
45

46
47
48
49

38
39
40
41
42
43
44

45
46
47
48

49







-
+



-
+
This is called ""[[parsed zettel|00001012053600]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'', but with the additional query parameter ''parseonly''.
Such a zettel was read and analyzed.
It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.]

However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'' and specifying an encoding.
The biggest difference to a parsed zettel is the inclusion of [[block transclusions|00001007031100]] or [[inline transclusions|00001007040324]] for an evaluated zettel.
It can also be presented in various encoding, including the ""zmk"" encoding.
Evaluations also applies to metadata of a zettel, when appropriate.
Evaluations also applies to metadata of a zettel, if appropriate.

Please note, that searching for content is based on parsed zettel.
Transcluded content will only be found in transcluded zettel, but not in the zettel that transcluded the content.
However, you will easily pick up that zettel by following the [[backward|00001006020000#backward]] metadata key of the transcluded zettel.
However, you will easily pick up that zettel by follow the [[backward|00001006020000#backward]] metadata key of the transcluded zettel.

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







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

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

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

; [!author|''author'']
29
30
31
32
33
34
35
36

37
38
39






40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
29
30
31
32
33
34
35

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

55
56
57
58
59
60
61
62







-
+



+
+
+
+
+
+









-
+







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

  This is a computed value.
  There is no need to set it via Zettelstore.
  
  If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to a date/time; otherwise the version time of the running software will be used.
  If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used.

  Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second.
  When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible.
; [!created-missing|''created-missing'']
: If set to ""true"", the value of [[''created''|#created]] was not stored within a zettel.
  To allow the migration of [[zettel identifier|00001006050000]] to a new scheme, you should update the value of ''created'' to a reasonable value.
  Otherwise you might lose that information in future releases.

  This key will be removed when the migration to a new zettel identifier format has been completed.
; [!credential|''credential'']
: Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]].
  It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key.

  It is only used for zettel with a ''role'' value of ""user"".
; [!dead|''dead'']
: Property that contains all references that does __not__ identify a zettel.
; [!expire|''expire'']
: A user-entered time stamp that document the point in time when the zettel should expire.
  When a zettel expires, Zettelstore does nothing.
  When a zettel is expires, Zettelstore does nothing.
  It is up to you to define required actions.
  ''expire'' is just a documentation.
  You could define a query and execute it regularly, for example [[query:expire? ORDER expire]].
  Alternatively, a Zettelstore client software could define some actions when it detects expired zettel.
; [!folge|''folge'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value.
; [!folge-role|''folge-role'']
81
82
83
84
85
86
87
88

89
90

91
92
93
94
95
96
97
87
88
89
90
91
92
93

94
95

96
97
98
99
100
101
102
103







-
+

-
+







  Basically the inverse of key [[''folge''|#folge]].
; [!predecessor|''predecessor'']
: References the zettel that contains a previous version of the content.
  In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons.
  Basically the inverse of key [[''successors''|#successors]].
; [!prequel|''prequel'']
: Specifies a zettel that is conceptually a prequel zettel.
  This is a zettel that occurred somehow before the current zettel.
  This is a zettel that occured somehow before the current zettel.
; [!published|''published'']
: This property contains the timestamp of the last modification / creation of the zettel.
: This property contains the timestamp of the mast modification / creation of the zettel.
  If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value.
  Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value.
  Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used.
  In all other cases, this property is not set.

  It can be used for [[sorting|00001007700000]] zettel based on their publication date.

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







-
-







-
-
+
+
-
-











-
+







; [!role|''role'']
: Defines the role of the zettel.
  Can be used for selecting zettel.
  See [[supported zettel roles|00001006020100]].
  If not given, it is ignored.
; [!sequel|''sequel'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] value.
; [!subordinates|''subordinates'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value.
; [!successors|''successors'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value.
  Therefore, it references all zettel that contain a new version of the content and/or metadata.
  In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons.
  In most cases, zettel referencing the current zettel should be updated to reference a successor zettel.
  The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel.
; [!summary|''summary'']
: Summarizes the content of the zettel using plain text.
; [!superior|''superior'']
: Summarizes the content of the zettel.
  You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup.
: Specifies a zettel that is conceptually a superior zettel.
  This might be a more abstract zettel, or a zettel that should be higher in a hierarchy.
; [!syntax|''syntax'']
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If it is not given, it defaults to ''plain''.
; [!tags|''tags'']
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must begin with the number sign character (""''#''"", U+0023).
; [!title|''title'']
: Specifies the title of the zettel.
  If not given, the value of [[''id''|#id]] will be used.
; [!url|''url'']
: Defines a URL / URI for this zettel that possibly references external material.
: Defines an URL / URI for this zettel that possibly references external material.
  One use case is to specify the document that the current zettel comments on.
  The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template.
; [!useless-files|''useless-files'']
: Contains the file names that are rejected to serve the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].
  If a zettel is deleted, these files will also be deleted.
; [!user-id|''user-id'']

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

7
8
9
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
created: 20210126175322
modified: 20250102175032
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 hierarchy of meta-roles.
  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 hierarchy of meta-tags.
  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'']

Changes to docs/manual/00001006020400.zettel.

1
2
3
4
5
6
7

8
9
10
11


12
13
14
15


16
17
18




19
20
21


22
23


24
25
26
27
28
29
30
31
32
33
34





1
2
3
4
5
6

7
8
9
10

11
12
13
14
15

16
17
18


19
20
21
22
23
24

25
26
27

28
29
30
31
32
33
34
35
36
37



38
39
40
41
42






-
+



-
+
+



-
+
+

-
-
+
+
+
+


-
+
+

-
+
+








-
-
-
+
+
+
+
+
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102205707
modified: 20211124132040

A zettel can be marked as read-only, if it contains a metadata value for key
[[''read-only''|00001006020000#read-only]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel, depending on their [[user role|00001010070300]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel,
depending on their [[user role|00001010070300]].
Otherwise, the read-only mark is just a binary value.

=== No authentication
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel.
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]]
is interpreted as ""false"", anybody can modify the zettel.

If the metadata value is something else (the value ""true"" is recommended), the user cannot modify the zettel through the [[web user interface|00001014000000]].
However, if the zettel is stored as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor.
If the metadata value is something else (the value ""true"" is recommended),
the user cannot modify the zettel through the [[web user interface|00001014000000]].
However, if the zettel is stored as a file in a [[directory box|00001004011400]],
the zettel could be modified using an external editor.

=== Authentication enabled
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel.
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]]
is interpreted as ""false"", anybody can modify the zettel.

If the metadata value is the same as an explicit [[user role|00001010070300]], users with that role (or a role with lower rights) are not allowed to modify the zettel.
If the metadata value is the same as an explicit [[user role|00001010070300]],
users with that role (or a role with lower rights) are not allowed to modify the zettel.

; ""reader""
: Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel.
  Users with role ""writer"" or the owner itself still can modify the zettel.
; ""writer""
: Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel.
  Only the owner of the Zettelstore can modify the zettel.

If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended), no user is allowed to modify the zettel through the [[web user interface|00001014000000]].
However, if the zettel is accessible as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor.
Typically the owner of a Zettelstore has such access.
If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended),
no user is allowed modify the zettel through the [[web user interface|00001014000000]].
However, if the zettel is accessible as a file in a [[directory box|00001004011400]],
the zettel could be modified using an external editor.
Typically the owner of a Zettelstore have such an access.

Changes to docs/manual/00001006030000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17

18
19
20
21
22
23
24
1
2
3
4
5
6

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






-
+










+







id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250115172354
modified: 20240219161909

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

User-defined metadata keys conform also to a type, based on the suffix of the key.

|=Suffix|Type
| ''-date'' | [[Timestamp|00001006034500]]
| ''-number'' | [[Number|00001006033000]]
| ''-role'' | [[Word|00001006035500]]
| ''-time'' | [[Timestamp|00001006034500]]
| ''-title'' | [[Zettelmarkup|00001006036500]]
| ''-url'' | [[URL|00001006035000]]
| ''-zettel''  | [[Identifier|00001006032000]]
| ''-zid''  | [[Identifier|00001006032000]]
| ''-zids''  | [[IdentifierSet|00001006032500]]
| any other suffix | [[EString|00001006031500]]

The name of the metadata key is bound to the key type
33
34
35
36
37
38
39

34
35
36
37
38
39
40
41







+
* [[IdentifierSet|00001006032500]]
* [[Number|00001006033000]]
* [[String|00001006033500]]
* [[TagSet|00001006034000]]
* [[Timestamp|00001006034500]]
* [[URL|00001006035000]]
* [[Word|00001006035500]]
* [[Zettelmarkup|00001006036500]]

Changes to docs/manual/00001006031500.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001006031500
title: EString Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20250102164729
modified: 20230419175525

Values of this type are just a sequence of characters, possibly an empty sequence.
Values of this type are just a sequence of character, possibly an empty sequence.

An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].]

=== Allowed values
All printable characters are allowed.

=== Query comparison

Changes to docs/manual/00001006033000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17

18
19
20
21
22
23
24
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24






-
+









-
+







id: 00001006033000
title: Number Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20250102220057
modified: 20230612183900

Values of this type denote a numeric integer value.

=== Allowed values
Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character.

=== Query comparison
[[Search operators|00001007705000]] for equality (""equal"" or ""not equal"", ""has"" or ""not has""), for lesser values (""less"" or ""not less""), or for greater values (""greater"" or ""not greater"") are executed by converting both the [[search value|00001007706000]] and the metadata value into integer values and then comparing them numerically.
Integer values must be in the range -9223372036854775808 &hellip; 9223372036854775807.
Comparisons with metadata values outside this range always return a negative match.
Comparisons with metadata values outside this range always returns a negative match.
Comparisons with search values outside this range will be executed as a comparison of the string representation values.

All other comparisons (""match"", ""not match"", ""prefix"", ""not prefix"", ""suffix"", and ""not suffix"") are done on the given string representation of the number.
In this case, the number ""+12"" will be treated as different to the number ""12"".

=== Sorting
Sorting is done by comparing the numeric values.

Changes to docs/manual/00001006034000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14

15
16
17
18
19
20
21
1
2
3
4
5
6

7
8
9
10
11
12
13

14
15
16
17
18
19
20
21






-
+






-
+







id: 00001006034000
title: TagSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20250102205826
modified: 20230419175642

Values of this type denote a (sorted) set of tags.

A set is different to a list, as no duplicate values are allowed.

=== Allowed values
Every tag must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character.
Every tag must must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character.
Tags are separated by space characters.

All characters are mapped to their lower case values.

=== Query comparison
All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"".

Changes to docs/manual/00001006035000.zettel.

1
2
3
4
5
6
7

8
9

10
11
12

13
14
15
16
17
18
19
1
2
3
4
5
6

7
8

9
10
11

12
13
14
15
16
17
18
19






-
+

-
+


-
+







id: 00001006035000
title: URL Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20250102205855
modified: 20230419175725

Values of this type denote a URL.
Values of this type denote an URL.

=== Allowed values
All characters of a URL / URI are allowed.
All characters of an URL / URI are allowed.

=== Query comparison
All comparisons are done case-insensitive.
For example, ""hello"" is the suffix of ""http://example.com/Hello"".

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

Added docs/manual/00001006036500.zettel.




























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001006036500
title: Zettelmarkup Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175441

Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]].

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Query comparison
Comparison is done similar to the full-text search: both the value to compare and the metadata value are normalized according to Unicode NKFD, ignoring everything except letters and numbers.
Letters are mapped to the corresponding lower-case value.

For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001006050000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14


15
16
17
18
19
20
21


22
23
24
25

26
27


1
2
3
4
5
6

7
8
9
10
11
12
13

14
15
16
17
18
19
20
21

22
23
24
25
26

27
28

29
30






-
+






-
+
+






-
+
+



-
+

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

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the next two represent the month, followed by day, hour, minute, and second.
They resemble a timestamp: the first four digits could represent the year, the
next two represent the month, following by day, hour, minute, and second.

This allows to order zettel chronologically in a canonical way.

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

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

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

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

Changes to docs/manual/00001006055000.zettel.

1
2
3
4
5
6
7

8
9

10
11
12
13
14

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

1
2
3
4
5
6

7
8

9
10
11
12
13

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

33






-
+

-
+




-
+


















-
+
id: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210721105704
modified: 20250102222416
modified: 20241202100917

[[Zettel identifier|00001006050000]] are typically created by examining the current date and time.
[[Zettel identifier|00001006050000]] are typically created by examine the current date and time.
By renaming the name of the underlying zettel file, you are able to provide any sequence of 14 digits.

To make things easier, you must not use zettel identifier that begin with four zeroes (''0000'').

All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel is ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.].
All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.].
Zettel identifier of this manual have be chosen to begin with ''000010''.

However, some external applications may need at least one defined zettel identifier to work properly.
Zettel [[Zettelstore Application Directory|00000999999999]] (''00000999999999'') can be used to associate a name to a zettel identifier.
For example, if your application is named ""app"", you create a metadata key ''app-zid''.
Its value is the zettel identifier of the zettel that configures your application.

=== Reserved Zettel Identifier

|= From | To | Description
| 00000000000000 | 00000000000000 | This is an invalid zettel identifier
| 00000000000001 | 00000999999999 | [[Predefined zettel|00001005090000]]
| 00001000000000 | 00001099999999 | This [[Zettelstore manual|00001000000000]]
| 00001100000000 | 00008999999999 | Reserved, do not use
| 00009000000000 | 00009999999999 | Reserved for applications

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

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


1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
16
17
18

19
20
21
22

23
24
25
26

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

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

54
55
56
57
58
59


60
61






-
+

-
+









-
+



-
+



-
+














-
+











-
+





-
-
+
+
id: 00001007010000
title: Zettelmarkup: General Principles
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250106174703
modified: 20241213101524

Any document can be thought of as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks.
Any document can be thought as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks.
Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs.
Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images.

With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters.
List blocks also begin at the first position of a line, but may need one or more identical character, plus a space character.
[[Table blocks|00001007031000]] begin at the first position of a line with the character ""``|``"".
Non-list blocks are either fully specified on that line or they span multiple lines and are delimited with the same three or more character.
It depends on the block kind, whether blocks are specified on one line or on at least two lines.

If a line does not begin with an explicit block element, the line is treated as a (implicit) paragraph block element that contains inline elements.
If a line does not begin with an explicit block element. the line is treated as a (implicit) paragraph block element that contains inline elements.
This paragraph ends when a block element is detected at the beginning of a next line or when an empty line occurs.
Some blocks may also contain inline elements, e.g. a heading.

Inline elements mostly begin with two non-space, often identical characters.
Inline elements mostly begins with two non-space, often identical characters.
With some exceptions, two identical non-space characters begins a formatting range that is ended with the same two characters.

Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"".
A link is given with ``[[...]]``{=zmk}, an image with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}.
A link is given with ``[[...]]``{=zmk}, an images with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}.
An inline comment, beginning with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins.
The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If put at the end of non-space text.].

Some inline elements do not follow the rule of two identical character, especially to specify [[footnotes|00001007040330]], [[citation keys|00001007040340]], and local marks.
These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``"").

One inline element that does not begin with two characters is the ""entity"".
It allows to specify any Unicode character.
The specification of that character is put between an ampersand character and a semicolon: ``&...;``{=zmk}.
For example, an ""n-dash"" could also be specified as ``&ndash;``{==zmk}.

The backslash character (""``\\``"") possibly gives the next character a special meaning.
This allows to resolve some left ambiguities.
For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}.
An inline element to strongly emphasize some text that begins with a space will be specified as ``** Text**``{=zmk}.
An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}.
To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified.

Many block and inline elements can be refined by additional [[attributes|00001007050000]].
Attributes resemble roughly HTML attributes and are put near the corresponding elements by using the syntax ``{...}``{=zmk}.
One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``.

To summarize:

* With some exceptions, block-structural elements begins at the for position of a line with three identical characters.
* The most important exception to this rule is the specification of lists.
* If no block element is found, a paragraph with inline elements is assumed.
* With some exceptions, inline-structural elements begin with two characters, quite often the same two characters.
* With some exceptions, inline-structural elements begins with two characters, quite often the same two characters.
* The most important exceptions are links.
* The backslash character can help to resolve possible ambiguities.
* Attributes refine some block and inline elements.
* Block elements have a higher priority than inline elements.

These principles make automatic recognizing zettelmarkup an (relatively) easy task.
By looking at the reference implementation, a moderately skilled software developer should be able to create an appropriate software in a different programming language.
These principles makes automatic recognizing zettelmarkup an (relatively) easy task.
By looking at the reference implementation, a moderately skilled software developer should be able to create a appropriate software in a different programming language.

Changes to docs/manual/00001007030100.zettel.

1
2
3
4
5
6
7

8
9
10

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

7
8
9

10
11
12
13
14
15
16
17






-
+


-
+







id: 00001007030100
title: Zettelmarkup: Description Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102180137
modified: 20220218131155

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

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

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

Changes to docs/manual/00001007030300.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001007030300
title: Zettelmarkup: Headings
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102210039
modified: 20220218133755

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

```zmk
=== Level 1 Heading
26
27
28
29
30
31
32
33

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

33
34
35
36
37
38
39
40
41
42
43







-
+










====== Level 4 Heading
======= Level 5 Heading
======== Level 5 Heading
:::

=== Notes

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

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

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

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

Changes to docs/manual/00001007030400.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14

15
16
17
18
19
20
21
1
2
3
4
5
6

7
8
9
10
11
12
13

14
15
16
17
18
19
20
21






-
+






-
+







id: 00001007030400
title: Zettelmarkup: Horizontal Rules / Thematic Break
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102220220
modified: 20220825185533

To signal a thematic break, you can specify a horizontal rule.
This is done by entering at least three hyphen-minus characters (""''-''"", U+002D) at the first position of a line.
You can add some [[attributes|00001007050000]], although the horizontal rule does not support the default attribute.
Any other characters in this line will be ignored.

If you do not enter the three hyphen-minus character at the very first position of a line, they are interpreted as [[inline elements|00001007040000]], typically as an ""en-dash"" followed by a hyphen-minus.
If you do not enter the three hyphen-minus character at the very first position of a line, the are interpreted as [[inline elements|00001007040000]], typically as an ""en-dash" followed by a hyphen-minus.

Example:

```zmk
---
----{.zs-deprecated}
-----

Changes to docs/manual/00001007030800.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001007030800
title: Zettelmarkup: Region Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102180106
modified: 20220323190829

Region blocks do not directly have a visual representation.
Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.

This kind of line-range block begins with at least three colon characters (""'':''"", U+003A) at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.].
You can add some [[attributes|00001007050000]] on the beginning line of a region block, following the initiating characters.
The region block does not support the default attribute, but it supports the generic attribute.

Changes to docs/manual/00001007030900.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001007030900
title: Zettelmarkup: Comment Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102222357
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.
30
31
32
33
34
35
36
37

30
31
32
33
34
35
36

37







-
+
```
will be completely ignored, while
```zmk
%%%{-}
Will be rendered
%%%
```
will be rendered as some kind of comment[^This cannot be shown here, because an HTML comment will not be rendered visible; it will be in the HTML text.].
will be rendered as some kind of comment[^This cannot be shown here, because a HTML comment will not be rendered visible; it will be in the HTML text.].

Changes to docs/manual/00001007031000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001007031000
title: Zettelmarkup: Tables
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102210107
modified: 20241212153641

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

29
30
31
32
33
34
35
36

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

36
37
38
39
40
41
42
43







-
+







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

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

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

Changes to docs/manual/00001007031110.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
1
2
3
4
5
6

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

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: 00001007031110
title: Zettelmarkup: Zettel Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20250102165258
modified: 20220926183331

A zettel transclusion is specified by the following sequence, starting at the first position in a line: ''{{{zettel-identifier}}}''.

When evaluated, the referenced zettel is read.
If it contains some transclusions itself, these will be expanded, recursively.
When a recursion is detected, expansion does not take place.
Instead an error message replaces the transclude specification.

An error message is also given, if the zettel cannot be read or if too many transclusions are made.
The maximum number of transclusion can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel.

If everything went well, the referenced, expanded zettel will replace the transclusion element.

For example, to include the text of the Zettel titled ""Zettel identifier"", just specify its identifier [[''00001006050000''|00001006050000]] in the transclusion element:
For example, to include the text of the Zettel titled ""Zettel identifier"", just specify its identifier [[''00001006050000''|00001006050000]] in the transclude element:
```zmk
{{{00001006050000}}}
```
This will result in:
:::example
{{{00001006050000}}}
:::

Please note: if the referenced zettel is changed, all transclusions will also change.

This allows, for example, to create a bigger document just by transcluding smaller zettel.

In addition, if a zettel __z__ transcludes a zettel __t__, but the current user is not allowed to view zettel __t__ (but zettel __z__), then the transclusion will not take place.
To the current user, it seems that there was no transclusion in zettel __z__.
This allows to create a zettel with content that seems to be changed, depending on the authorization of the current user.

---
Any [[attributes|00001007050000]] added to the transclusion will set/overwrite the appropriate metadata of the included zettel.
Of course, this applies only to those attributes, which have a valid name for a metadata key.
Of course, this applies only to thoes attribtues, which have a valid name for a metadata key.
This allows to control the evaluation of the included zettel, especially for zettel containing a diagram description.

=== See also
[[Inline-mode transclusion|00001007040324]] does not work at the paragraph / block level, but is used for [[inline-structured elements|00001007040000]].

Changes to docs/manual/00001007031140.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20241213153229
modified: 20240219161800

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

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.
34
35
36
37
38
39
40









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







+
+
+
+
+
+
+
+
+







: The resulting list will be a numbered list.
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''TITLE'' (parameter)
: All words following ''TITLE'' are joined together to form a title.
  It is used for the ''ATOM'' and ''RSS'' action.
; ''ATOM'' (aggregate)
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'', ''REINDEX'' (aggregate)
: Will be ignored.
  These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful).
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.

Changes to docs/manual/00001007031200.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16

17
18
19
20
21
22
23
1
2
3
4
5
6

7
8
9
10
11
12

13
14
15

16
17
18
19
20
21
22
23






-
+





-
+


-
+







id: 00001007031200
title: Zettelmarkup: Inline-Zettel Block
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220201142439
modified: 20250102183744
modified: 20221018121251

An inline-zettel block allows to specify some content with another syntax without creating a new zettel.
This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup.
Another example is to specify HTML code to use it for some kind of web front-end framework.

Like all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line.
As all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line.
For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used.

You can add some [[attributes|00001007050000]] to the beginning line of a verbatim block, following the initiating characters.
You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The inline-zettel block uses the attribute key ""syntax"" to specify the [[syntax|00001008000000]] of the inline-zettel.
Alternatively, you can use the generic attribute to specify the syntax value.
If no value is provided, ""[[text|00001008000000#text]]"" is assumed.

Any other character in this first 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 at-sign characters given at the beginning line.

Changes to docs/manual/00001007040000.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
1
2
3
4
5
6

7
8


9
10
11
12
13
14
15
16
17
18
19

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

38
39
40
41
42
43
44
45
46
47

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

60
61
62
63
64
65






-
+

-
-
+
+









-
+

















-
+









-
+











-
+





id: 00001007040000
title: Zettelmarkup: Inline-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102182659
modified: 20220920143243

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

; Text formatting
: Every [[text formatting|00001007040100]] element begins with two same characters at the beginning.
  It lasts until the same two characters occurred the second time.
  Some of these elements explicitly support [[attributes|00001007050000]].

; Literal-like formatting
: Sometimes, you want to enter the text as it is.
: Sometime you want to enter the text as it is.
: This is the core motivation of [[literal-like formatting|00001007040200]].

; Reference-like text
: You can reference other zettel and (external) material within one zettel.
  This kind of reference may be a link, or an images that is display inline when the zettel is rendered.
  Footnotes sometimes factor out some useful text that hinders the flow of reading text.
  Internal marks allow to reference something within a zettel.
  An important aspect of all knowledge work is to reference others work, e.g. with citation keys.
  All these elements can be subsumed under [[reference-like text|00001007040300]].

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

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

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

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

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

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

According to the [[HTML Standard|https://html.spec.whatwg.org/multipage/syntax.html#character-references]], some numeric code points are not allowed.
These are all code point below the numeric value 32 (decimal) or 0x20 (hex) and all code points for [[noncharacter|https://infra.spec.whatwg.org/#noncharacter]] values.

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

* Two consecutive hyphen-minus characters result in an __en-dash__ character.
  It is typically used in numeric ranges.
  ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}.
  Alternative specifications are: ``&ndash;``, ``&x8211``, and ``&#x2013``.

Changes to docs/manual/00001007040100.zettel.

1
2
3
4
5
6
7

8
9
10

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

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

37
1
2
3
4
5
6

7
8
9

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

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

36
37






-
+


-
+













-
+











-
+

id: 00001007040100
title: Zettelmarkup: Text Formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250106174436
modified: 20231113191353

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

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

The following characters begin a text formatting:

* The low line character (""''_''"", U+005F) emphasizes its text.
** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}.
* The asterisk character (""''*''"", U+002A) strongly emphasized its enclosed text.
** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}.
* The greater-than sign character (""''>''"", U+003E) marks text as inserted.
** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}.
* Similarly, the tilde character (""''~''"", U+007E) marks deleted text.
* Similar, the tilde character (""''~''"", U+007E) marks deleted text.
** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}.
* 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) marks some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements.
* 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/00001007040200.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001007040200
title: Zettelmarkup: Literal-like formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250115180425
modified: 20220311185110

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.
* Mark text as input you give into a computer via a keyboard.
* Mark text as output from some computer, e.g. shown at the command line.

=== Literal text
43
44
45
46
47
48
49














50
51
52
53

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

67
68
69
70
71
72
73
74







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



-
+








Examples:
* ``==The result is: 42==`` renders in HTML as ::==The result is: 42==::{=example}.
* ``==The result is: 42=={-}`` renders in HTML as ::==The result is: 42=={-}::{=example}.

Attributes can be specified, the default attribute has the same semantic as for literal text.

=== Inline-zettel snippet
To specify an inline snippet in a different [[syntax|00001008000000]], delimit your text with two at-sign characters (""''@''"", U+0040) on each side.

You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use.
Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value.
If no value is provided, ""[[text|00001008000000#text]]"" is assumed.

Examples:
* ``A @@-->@@ B`` renders in HTML as ::A @@-->@@ B::{=example}.
* ``@@<small>@@{=html}Small@@</small>@@{=html}`` renders in HTML as ::@@<small>@@{=html}Small@@</small>@@{=html}::{=example}.

To some degree, an inline-zettel snippet is the @@<small>@@{=html}smaller@@</small>@@{=html} sibling of the [[inline-zettel block|00001007031200]].
For HTML syntax, the same rules apply.

=== Math mode / $$\TeX$$ input
This allows to enter text, that is typically interpreted by $$\TeX$$ or similar software.
The main difference to all other literal-like formatting above is that the backslash character (""''\\''"", U+005C) has no special meaning.
Therefore it is well suited to enter text with a lot of backslash characters.
Therefore it is well suited the enter text with a lot of backslash characters.

Math mode text is delimited with two dollar signs (""''$''"", U+0024) on each side.

You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use.
Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value.
If no syntax value is provided, math mode text roughly corresponds to literal text.

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

1
2
3
4
5
6

7
8
9

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

27
28
29
30
31
32
33
34

35
36
37

38






-
+


-
+
















-
+







-
+


-
+
id: 00001007040310
title: Zettelmarkup: Links
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20250102183944
modified: 20221024173849

There are two kinds of links, regardless of links to (internal) other zettel or to (external) material.
Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and end with two consecutive right square bracket characters (""'']''"", U+005D).
Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and ends with two consecutive right square bracket characters (""'']''"", U+005D).
If the content starts with more than two left square bracket characters, all but the last two will be treated as text.

The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``.
The text is a sequence of [[inline elements|00001007040000]].
However, it should not contain links itself.

The second form just provides a link specification between the square brackets.
Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``.

=== Link specifications
The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]].
To reference some content within a zettel, you can append a number sign character (""''#''"", U+0023) and the name of the mark to the zettel identifier.
The resulting reference is called ""zettel reference"".

If the link specification begins with the string ''query:'', the text following this string will be interpreted as a [[query expression|00001007700000]].
The resulting reference is called ""query reference"".
When this type of reference is rendered, it will typically reference a list of all zettel that fulfills the query expression.
When this type of references is rendered, it will typically reference a list of all zettel that fulfills the query expression.

A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character,
will be interpreted as a local reference, called __hosted reference__.
Such references will be interpreted relative to the web server hosting the Zettelstore.

If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]].

To specify some material outside the Zettelstore, just use a normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].
To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].

=== Other topics
If the link references another zettel, and this zettel is not readable for the current user, e.g. because of missing access rights, then only the associated text is presented.
If the link references another zettel, and this zettel is not readable for the current user, because of a missing access rights, then only the associated text is presented.

Changes to docs/manual/00001007040320.zettel.

1
2
3
4
5
6
7

8
9

10
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25






-
+

-
+








-
+







id: 00001007040320
title: Zettelmarkup: Inline Embedding / Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20250102210158
modified: 20221024173926

To some degree, a specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]].
To some degree, an specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]].
Both contain a reference specification and optionally some text.
In contrast to a link, the specification of embedded material must currently resolve to some kind of real content.
This content replaces the embed specification.

An embed specification begins with two consecutive left curly bracket characters (""''{''"", U+007B) and ends with two consecutive right curly bracket characters (""''}''"", U+007D).
The curly brackets delimits either a reference specification or some text, a vertical bar character and the link specification, similar to a link.
If the content starts with more than two left curly bracket characters, all but the last two will be treated as text.

One difference to a link: if the text is not given, an empty string is assumed.
One difference to a link: if the text was not given, an empty string is assumed.

The reference must point to some content, either zettel content or URL-referenced content.
If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored.
If the referenced zettel does not exist, or is not readable because of other reasons, a [[spinning emoji|00000000040001]] is presented as a visual hint:
 
Example: ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}.

Changes to docs/manual/00001007040322.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20250102222115
modified: 20241202101206

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

Supported formats are:

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

Changes to docs/manual/00001007040324.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
1
2
3
4
5
6

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

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

36
37
38
39
40
41
42
43
44

45
46
47
48
49






-
+













-
+














-
+








-
+




id: 00001007040324
title: Zettelmarkup: Inline-mode Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20250102183508
modified: 20231222164501

Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content.
For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]).

Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements.

First, the referenced zettel is read.
If it contains other transclusions, these will be expanded, recursively.
When an endless recursion is detected, expansion does not take place.
Instead an error message replaces the transclude specification.

The result of this (indirect) transclusion is searched for inline-structured elements.

* If only a [[zettel identifier|00001006050000]] was specified, the first top-level [[paragraph|00001007030000#paragraphs]] is used.
* If only an [[zettel identifier|00001006050000]] was specified, the first top-level [[paragraph|00001007030000#paragraphs]] is used.
  Since a paragraph is basically a sequence of inline-structured elements, these elements will replace the transclude specification.

  Example: ``{{00010000000000}}`` (see [[00010000000000]]) is rendered as ::{{00010000000000}}::{=example}.

* If a fragment identifier was additionally specified, the element with the given fragment is searched:
** If it specifies a [[heading|00001007030300]], the next top-level paragraph is used.

   Example: ``{{00010000000000#reporting-errors}}`` is rendered as ::{{00010000000000#reporting-errors}}::{=example}.

** In case the fragment names a [[mark|00001007040350]], the inline-structured elements after the mark are used.
   Initial spaces and line breaks are ignored in this case.

   Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}.

** Just specifying the fragment identifier will reference something on the current page.
** Just specifying the fragment identifier will reference something in the current page.
   This is not allowed, to prevent a possible endless recursion.

* If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered.

  Example: ``{{//z/00000000040001}}{alt=Emoji}`` is rendered as ::{{//z/00000000040001}}{alt=Emoji}::{=example}

If no inline-structured elements are found, the transclude specification is replaced by an error message.

To avoid an exploding ""transclusion bomb"", a form of [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited.
To avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited.
The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel.

=== See also
[[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]].

Changes to docs/manual/00001007040340.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16
1
2
3
4
5
6

7
8
9
10
11
12

13
14
15
16






-
+





-
+



id: 00001007040340
title: Zettelmarkup: Citation Key
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20250102210258
modified: 20220218133447

A citation key references some external material that is part of a bibliographical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), the citation key is given.
However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given.
The key is typically a sequence of letters and digits.
If a comma character (""'',''"", U+002C) or a vertical bar character is given, the following is interpreted as [[inline elements|00001007040000]].
A right square bracket ends the text and the citation key element.

Changes to docs/manual/00001007701000.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001007701000
title: Query: Search Expression
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707205043
modified: 20250102210324
modified: 20230707210039

In its simplest form, a search expression just contains a string to be searched for with the help of a full-text search.
In its simplest form, a search expression just contains a string to be search for with the help of a full-text search.
For example, the string ''syntax'' will search for all zettel containing the word ""syntax"".

If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''.
""title"" denotes the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]].
The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match.
""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"".

Changes to docs/manual/00001007702000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
1
2
3
4
5
6

7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22






-
+







-
+







id: 00001007702000
title: Search term
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20250102210348
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]].

  All zettel containing the given metadata key with an allowed value (depending on the search operator) are selected.
  All zettel containing the given metadata key with a allowed value (depending on the search operator) are selected.

  If no search value is given, then all zettel containing the given metadata key are selected (or ignored, for a negated search operator).
* An optional [[search operator|00001007705000]], followed by a [[search value|00001007706000]].

  This specifies a full-text search for the given search value.

  However, the operators ""less"" and ""greater"" are not supported, they are internally translated into the ""match"" operators.

Changes to docs/manual/00001007705000.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
1
2
3
4
5
6

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


20
21
22
23
24
25
26
27
28






-
+












-
-
+
+







id: 00001007705000
title: Search operator
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20250102210427
modified: 20230612180539

A search operator specifies how the comparison of a search value and a zettel should be executed.
Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters.

The following are allowed search operator characters:
* The exclamation mark character (""''!''"", U+0021) negates the meaning.
* The equal sign character (""''=''"", U+003D) compares on equal content (""equals operator"").
* The tilde character (""''~''"", U+007E) compares on matching (""match operator"").
* The left square bracket character (""''[''"", U+005B) matches if there is some prefix (""prefix operator"").
* The right square bracket character (""'']''"", U+005D) compares a suffix relationship (""suffix operator"").
* The colon character (""'':''"", U+003A) compares depending on the on the actual [[key type|00001006030000]] (""has operator"").
  In most cases, it acts as a equals operator, but for some type it acts as the match operator.
* The less-than sign character (""''<''"", U+003C) matches if the search value is somehow less than the metadata value (""less operator"").
* The greater-than sign character (""''>''"", U+003E) matches if the search value is somehow greater than the metadata value (""greater operator"").
* The less-than sign character (""''<''"", U+003C) matches if the search value is somehow less then the metadata value (""less operator"").
* The greater-than sign character (""''>''"", U+003E) matches if the search value is somehow greater then the metadata value (""greater operator"").
* The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"").
  In this case no [[search value|00001007706000]] must be given.

Since the exclamation mark character can be combined with the other, there are 18 possible combinations:
# ""''!''"": is an abbreviation of the ""''!~''"" operator.
# ""''~''"": is successful if the search value matched the value to be compared.
# ""''!~''"": is successful if the search value does not match the value to be compared.

Changes to docs/manual/00001007720300.zettel.

1
2
3
4
5
6
7

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

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


19
20
21
22
23
24
25






-
+











-
-







id: 00001007720300
title: Query: Context Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707204706
modified: 20250202172633
modified: 20241118174741

A context directive calculates the __context__ of a list of zettel identifier.
It starts with the keyword ''CONTEXT''.

Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters.
These are:
* ''FULL'': additionally search for zettel with the same tags,
* ''BACKWARD'': search for context only though backward links,
* ''FORWARD'': search for context only through forward links,
* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17),
* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200).
* ''MIN'': one or more space characters, and a positive integer: set the minimum number of context zettel (default: 0).
  Takes precedence over ''COST'' and ''MAX''.

If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links.

The cost of a context zettel is calculated iteratively:
* Each of the specified zettel hast a cost of one.
* A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus 0.1.
* A zettel found as a single sequel zettel or single prequel zettel has the cost of the originating zettel, plus 1.0.

Changes to docs/manual/00001007780000.zettel.

1
2
3
4
5
6
7

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

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

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

23

24
25
26
27
28
29
30






-
+















-
+
-







id: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20250202164337
modified: 20240219155949

```
QueryExpression   := ZettelList? QueryDirective* SearchExpression ActionExpression?
ZettelList        := (ZID (SPACE+ ZID)*).
ZID               := '0'+ ('1' .. '9'') DIGIT*
                   | ('1' .. '9') DIGIT*.
QueryDirective    := ContextDirective
                   | IdentDirective
                   | ItemsDirective
                   | UnlinkedDirective.
ContextDirective  := "CONTEXT" (SPACE+ ContextDetail)*.
ContextDetail     := "FULL"
                   | "BACKWARD"
                   | "FORWARD"
                   | "COST" SPACE+ PosInt
                   | "MAX" SPACE+ PosInt
                   | "MAX" SPACE+ PosInt.
                   | "MIN" SPACE+ PosInt.
IdentDirective    := IDENT.
ItemsDirective    := ITEMS.
UnlinkedDirective := UNLINKED (SPACE+ PHRASE SPACE+ Word)*.
SearchExpression  := SearchTerm (SPACE+ SearchTerm)*.
SearchTerm        := SearchOperator? SearchValue
                   | SearchKey SearchOperator SearchValue?
                   | SearchKey ExistOperator
41
42
43
44
45
46
47

48
49
50
51
52
53



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

53
54
55
56
57







+





-
+
+
+


                   | ('!')? ('~' | ':' | '[' | '}').
ExistOperator     := '?'
                   | '!' '?'.
PosInt            := '0'
                   | ('1' .. '9') DIGIT*.
ActionExpression  := '|' (Word (SPACE+ Word)*)?
Action            := Word
                   | 'ATOM'
                   | 'KEYS'
                   | 'N' NO-SPACE*
                   | 'MAX' PosInt
                   | 'MIN' PosInt
                   | 'REDIRECT'
                   | 'REINDEX'.
                   | 'REINDEX'
                   | 'RSS'
                   | 'TITLE' (SPACE Word)* .
Word              := NO-SPACE NO-SPACE*
```

Changes to docs/manual/00001007800000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001007800000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250115180517
modified: 20241125182149

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

|= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[marked / highlighted text|00001007040100]]
26
27
28
29
30
31
32
33

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

33
34
35
36
37
38
39
40
41
42
43







-
+










| ''/''  | (free) | (free)
| '':''  | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]]
| '';''  | [[Description term|00001007030100]] | (free)
| ''<''  | [[Quotation block|00001007030600]] | (free)
| ''=''  | [[Headings|00001007030300]] | [[Computer output|00001007040200]]
| ''>''  | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]]
| ''?''  | (free) | (free)
| ''@''  | [[Inline-Zettel block|00001007031200]] | (reserved)
| ''@''  | [[Inline-Zettel block|00001007031200]] | [[Inline-zettel snippet|00001007040200#inline-zettel-snippet]]
| ''[''  | (reserved)  | [[Linked material|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]]
| '']''  | (reserved) | End of [[link|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''^''  | (free) | [[Super-scripted text|00001007040100]]
| ''_''  | (free) | [[Emphasized text|00001007040100]]
| ''`''  | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]]
| ''{''  | [[Transclusion|00001007031100]] | [[Embedded material|00001007040300]], [[Attribute|00001007050000]]
| ''|''  | [[Table row / table cell|00001007031000]] | Separator within link and [[embed|00001007040320]] formatting
| ''}''  | End of [[Transclusion|00001007031100]] | End of embedded material, End of Attribute
| ''~''  | [[Evaluation block|00001007031300]] | [[Deleted text|00001007040100]]

Changes to docs/manual/00001007906000.zettel.

1
2
3
4
5
6
7

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

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

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

23
24
25
26
27
28
29
30






-
+















-
+







id: 00001007906000
title: Zettelmarkup: Second Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220811115501
modified: 20250102221931
modified: 20220926183427

After you have [[learned|00001007903000]] the basic concepts and markup of Zettelmarkup (paragraphs, emphasized text, and lists), this zettel introduces you into the concepts of links, thematic breaks, and headings.

=== Links
A Zettelstore is much more useful, if you connect related zettel.
If you read a zettel later, this allows you to know about the context of a zettel.
[[Zettelmarkup|00001007000000]] allows you to specify such a connection.
A connection can be specified within a paragraph via [[Links|00001007040310]].

* A link always starts with two left square bracket characters and ends with two right square bracket characters: ''[[...]]''.
* Within these character sequences you specify the [[zettel identifier|00001006050000]] of the zettel you want to reference: ''[[00001007903000]]'' will connect to zettel containing the first steps into Zettelmarkup.
* In addition, you should give the link a more readable description.
  This is done by prepending the description before the reference and use the vertical bar character to separate both: ''[[First Steps|00001007903000]]''.

You are not restricted to reference your zettel.
Alternatively, you might specify a URL of an external website: ''[[Zettelstore|https://zettelstore.de]]''.
Alternatively, you might specify an URL of an external website: ''[[Zettelstore|https://zettelstore.de]]''.
Of course, if you just want to specify the URL, you are allowed to omit the description: ''[[https://zettelstore.de]]''

|= Zettelmarkup | Rendered output | Remark
| ''[[00001007903000]]'' | [[00001007903000]] | If no description is given, the zettel identifier acts as a description
| ''[[First Steps|00001007903000]]'' | [[First Steps|00001007903000]] | The description should be chosen so that you are not confused later
| ''[[https://zettelstore.de]]'' | [[https://zettelstore.de]] | A link to an external URL is rendered differently
| ''[[Zettelstore|https://zettelstore.de]]'' | [[Zettelstore|https://zettelstore.de]] | You can use any URL your browser is able to support

Changes to docs/manual/00001008010500.zettel.

1
2
3
4
5
6
7

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

20
21
22
23
24

25
26
27
28
29
30






1
2
3
4
5
6

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

19
20
21
22


23






24
25
26
27
28
29






-
+











-
+



-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
id: 00001008010500
title: CommonMark
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk
created: 20220113183435
modified: 20250115200458
modified: 20221018123145
url: https://commonmark.org/

[[CommonMark|https://commonmark.org/]] is a Markdown dialect, an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown by providing an unambiguous syntax specification for Markdown, together with a suite of comprehensive tests to validate implementation.

Time will show, if this attempt is successful.

However, CommonMark is a well specified Markdown dialect, in contrast to most (if not all) other dialects.
Other software adopts CommonMark somehow, notably [[GitHub Flavored Markdown|https://github.github.com/gfm/]] (GFM).
But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed.
Plus, they sometimes build on an older specification of CommonMark.

Zettelstore supports the latest CommonMark [[specification version 0.31.2 (2024-01-28)|https://spec.commonmark.org/0.31.2/]].
Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]].
If possible, Zettelstore will adapt to newer versions when they are available.

To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark.
Effectively, Markdown and CommonMark are super-sets of HTML.
Internally, CommonMark is translated into [[Zettelmarkup|00001007000000]].
Internally, CommonMark is translated into some kind of super-set of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself super-sets of HTML.]
Since Zettelmarkup supports HTML content only at the block level via [[Inline Zettel|00001007031200]], most uses of HTML within a CommonMark zettel are not translated as expected.
Instead, inline level HTML is translated into [[literal text|00001007040200#literal-text]] with a [[generic attribute|00001007050000]] set to ``html``.

Therefore, Zettelstore itself will not pass the CommonMark test suite fully.
However, no CommonMark language element except inline HTML will fail to be encoded as HTML.
In most cases, the differences are not visible for a user, but only by comparing the generated HTML code.
This Zettelmarkup super-set is later [[encoded|00001012920500]], often into [[HTML|00001012920510]].
Because Zettelstore HTML encoding philosophy differs a little bit to that of CommonMark, Zettelstore itself will not pass the CommonMark test suite fully.
However, no CommonMark language element will fail to be encoded as HTML.
In most cases, the differences are not visible for an user, but only by comparing the generated HTML code.

Be aware, depending on the value of the startup configuration key [[''insecure-html''|00001004010000#insecure-html]], HTML code found within a CommonMark document or within the mentioned kind of super-set of Zettelmarkup will typically be ignored for security-related reasons.

Changes to docs/manual/00001010000000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







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

Your zettel may contain sensitive content.
You probably want to ensure that only authorized persons can read and/or modify them.
Zettelstore ensures this in various ways.

=== Local first
The Zettelstore is designed to run on your local computer.
40
41
42
43
44
45
46
47

48
49
50
51
52

53
54
55
56
57
58

59
60
61
62

63
64
65
66
67
68
40
41
42
43
44
45
46

47
48
49
50
51

52
53
54
55
56
57

58
59
60
61

62
63
64
65
66
67
68







-
+




-
+





-
+



-
+






It is up to you.

If someone is authenticated as the owner of the Zettelstore (hopefully you), no restrictions apply.
But as an owner, you can create ""user zettel"" to allow others to access your Zettelstore in various ways.
Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the [[API|00001012000000]].

Additionally, you can specify that a zettel is publicly visible.
In this case, nobody has to authenticate themselves to see the contents of the note.
In this case no one has to authenticate itself to see the content of the zettel.
Or you can specify that a zettel is visible only to the owner.
In this case, no authenticated user will be able to read and change that protected zettel.

* [[Visibility rules for zettel|00001010070200]]
* [[User roles|00001010070300]] define basic rights of a user
* [[User roles|00001010070300]] define basic rights of an user
* [[Authorization and read-only mode|00001010070400]]
* [[Access rules|00001010070600]] define the policy which user is allowed to do what operation.

=== Encryption
When Zettelstore is accessed remotely, the messages that are sent between Zettelstore and the client must be encrypted.
Otherwise, an eavesdropper could fetch sensitive data, such as passwords or precious content that is not for the public.
Otherwise, an eavesdropper could fetch sensible data, such as passwords or precious content that is not for the public.

The Zettelstore itself does not encrypt messages.
But you can put a server in front of it, which is able to handle encryption.
Most generic web server software allow this.
Most generic web server software do allow this.

To enforce encryption, [[authenticated sessions|00001010040700]] are marked as secure by default.
If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration.
Otherwise, authentication will not work.

* [[Use a server for encryption|00001010090100]]

Changes to docs/manual/00001010040200.zettel.

1
2

3
4
5
6
7

8
9

10
11
12
13
14
15
16
1

2
3
4
5
6

7
8

9
10
11
12
13
14
15
16

-
+




-
+

-
+







id: 00001010040200
title: Creating a user zettel
title: Creating an user zettel
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102221859
modified: 20221205160251

All data used for authenticating a user is stored in a special zettel called ""user zettel"". 
All data to be used for authenticating a user is store in a special zettel called ""user zettel"". 
A user zettel must have set the following two metadata fields:

; ''user-id'' (""user identification"")
: The unique identification to be specified for authentication.
; ''credential''
: A hashed password as generated by the [[``zettelstore password``{=sh}|00001004051400]] command.

24
25
26
27
28
29
30
31

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

31
32
33
34
35
36
37







-
+






A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]].

A user zettel can only be created by the owner of the Zettelstore.

The owner should execute the following steps to create a new user zettel:

# Create a new zettel.
# Save the zettel to get an [[identifier|00001006050000]] for this zettel.
# Save the zettel to get a [[identifier|00001006050000]] for this zettel.
# Choose a unique identification for the user.
#* If the identifier is not unique, authentication will not work for this user.
# Execute the [[``zettelstore password``|00001004051400]] command.
#* You have to specify the user identification and the zettel identifier
#* If you should not know the password of the new user, send her/him the user identification and the user zettel identifier, so that the person can create the hashed password herself.
# Edit the user zettel and add the hashed password under the meta key ''credential'' and the user identification under the key ''user-id''.

Changes to docs/manual/00001010040400.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001010040400
title: Authentication process
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102222012
modified: 20211127174943

When someone tries to authenticate itself with a user identifier / ""user name"" and a password, the following process is executed:
When someone tries to authenticate itself with an user identifier / ""user name"" and a password, the following process is executed:

# If meta key ''owner'' of the configuration zettel does not have a valid [[zettel identifier|00001006050000]] as value, authentication fails.
# Retrieve all zettel, where the meta key ''user-id'' has the same value as the given user identification. If the list is empty, authentication fails.
# From above list, the zettel with the numerically smallest identifier is selected.
  Or in other words: the oldest zettel is selected[^This is done to prevent an attacker from creating a new note with the same user identification].
# If the zettel does not have a value for the meta key ''credential'', authentication fails.
# The value of the meta key ''credential'' is compared with the given password.

Changes to docs/manual/00001010040700.zettel.

1
2
3
4
5
6
7

8
9

10
11
12
13
14

15
16
17
18
19
20
21

22
23
24
25

26
27

1
2
3
4
5
6

7
8

9
10
11
12
13

14
15
16
17
18
19
20

21
22
23
24

25
26

27






-
+

-
+




-
+






-
+



-
+

-
+
id: 00001010040700
title: Access token
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102215422
modified: 20241213101607

If a user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller.
If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller.
Otherwise the user will not be recognized by Zettelstore.

If the user was authenticated via the [[web user interface|00001014000000]], the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]].
When the web browser is closed, these cookies are not saved.
If you want the web browser to store the cookie for the lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''.
If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''.

If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime.
The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration.
Every time a web page is displayed, a fresh token is created and stored inside the cookie.

If the user was authenticated via the API, the access token will be returned as the content of the response.
Typically, the lifetime of this token is shorter, e.g. 10 minutes.
Typically, the lifetime of this token is more short term, e.g. 10 minutes.
It is specified by the ''token-lifetime-api'' value of the startup configuration.
If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]].

If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), you must set the ''insecure-cookie'' value in the startup configuration to ''true''.
If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), you must set the ''insecure-cookie'' value of the startup configuration to ''true''.
In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text.
You could use such a scenario if you know all parties that access the local network where you access the Zettelstore.
You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore.

Changes to docs/manual/00001010070200.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001010070200
title: Visibility rules for zettel
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102170611
modified: 20220923104643

For every zettel you can specify under which condition the zettel is visible to others.
This is controlled with the metadata key [[''visibility''|00001006020000#visibility]].
The following values are supported:

; [!public|""public""]
: The zettel is visible to everybody, even if the user is not authenticated.
28
29
30
31
32
33
34
35

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

35
36
37
38
39
40
41
42







-
+







  This is for zettel with sensitive content that might irritate the owner.
  Computed zettel with internal runtime information are examples for such a zettel.

When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"".
One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]].
This is to ensure that the web interface looks nice even for not authenticated users.
Another is the zettel containing the Zettelstore [[license|00000000000004]].
The [[default image|00000000040001]], used if an image reference is invalid, is also publicly visible.
The [[default image|00000000040001]], used if an image reference is invalid, is also public visible.

Please note: if [[authentication is not enabled|00001010040100]], every user has the same rights as the owner of a Zettelstore.
This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]].
In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner"").
The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000096'' is stored with the visibility ""expert"".
If you want to show such a zettel, you must set ''expert-mode'' to true.

Changes to docs/manual/00001010070400.zettel.

1
2
3
4
5
6
7

8
9
10

11
12
13
14
15
16
17
18
19

20
21

1
2
3
4
5
6

7
8
9

10
11
12
13
14
15
16
17
18

19
20

21






-
+


-
+








-
+

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

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

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

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
1
2
3
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: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102222042
modified: 20240711183736

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

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

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 a user|00001012050200]] to obtain an access token
* [[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]]

Changes to docs/manual/00001012050200.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012050200
title: API: Authenticate a client
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102215926
modified: 20230412150544

Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]].
This token has to be used for other API calls.
It is valid for a relatively short amount of time, as configured with the key ''token-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (typically 10 minutes).

The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request:
```sh
24
25
26
27
28
29
30
31

32
33
34
35
36
37

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

51
52
53
54
55
56
57
58
24
25
26
27
28
29
30

31
32
33
34
35
36

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

50
51
52
53
54
55
56
57
58







-
+





-
+












-
+









If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data:
```sh
# curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4OCwiaWF0IjoxNjgxMzA0MDI4LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.qIEyOMFXykCApWtBaqbSESwTL96stWl2LRICiRNAXUjcY-mwx_SSl9L5Fj2FvmrI1K1RBvWehjoq8KZUNjhJ9Q" 600)
```

In all cases, you will receive a list containing three elements with all [[relevant data|00001012921000]] needed for further API calls.
In all cases, you will receive a list with three elements that will contain all [[relevant data|00001012921000]] to be used for further API calls.

**Important:** obtaining a token is a time-intensive process.
Zettelstore will delay every request to obtain a token for a certain amount of time.
Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more.

However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediately, without any delay:
However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediate, without any delay:

```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
("Bearer" "freeaccess" 316224000)
```

In this case, it is even possible to omit the user identification/password.

=== HTTP Status codes
In all cases of successful authentication, a list is returned, which contains the token as the second element.
A successful authentication is signaled with the HTTP status code 200, as usual.

Other status codes possibly sent by the Zettelstore:
Other status codes possibly send by the Zettelstore:
; ''400''
: Unable to process the request.
  In most cases the form data was invalid.
; ''401''
: Authentication failed.
  Either the user identification is invalid or you provided the wrong password.
; ''403''
: Authentication is not active.

Changes to docs/manual/00001012051200.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001012051200
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241216104355
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 a valid [[access token|00001012050200]] in the ''Authorization'' header].
To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
Always use the endpoint ''/z'' to work with a list of zettel.

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

```sh

Changes to docs/manual/00001012051400.zettel.

1
2
3
4
5
6
7

8
9
10

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

7
8
9

10
11
12
13
14
15
16
17






-
+


-
+







id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20241216104329
modified: 20240711161320
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions.
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''.

Changes to docs/manual/00001012053300.zettel.

1
2
3
4
5
6
7

8
9
10
11

12
13
14
15
16
17

18
19
20
21
22
23
24
1
2
3
4
5
6

7
8
9
10

11
12
13
14
15
16

17
18
19
20
21
22
23
24






-
+



-
+





-
+







id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20241216104429
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 a valid [[access token|00001012050200]] in the ''Authorization'' header].
For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

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

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

```sh
...
````

Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''part=PART''.

Changes to docs/manual/00001012053400.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20241216104120
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 a valid [[access token|00001012050200]] in the ''Authorization'' header].
The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

To retrieve the plain metadata of a zettel, use the query parameter ''part=meta''

````sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta'
title: API: Retrieve metadata of an existing zettel
role: manual

Changes to docs/manual/00001012053500.zettel.

1
2
3
4
5
6
7

8
9
10
11

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

7
8
9
10

11
12
13
14
15
16
17
18






-
+



-
+







id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20241216104145
modified: 20240620171057

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

For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''.
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'
(BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") (LITERAL-INPUT () "{ID}") (TEXT " is a placeholder for the ") ...
```

To select another encoding, you must provide the query parameter ''enc=ENCODING''.

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

7
8
9
10
11
12
13

14
15
16
17
18
19
20
21






-
+






-
+







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

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

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

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example, 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'
(BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.

Changes to docs/manual/00001012080200.zettel.

1
2
3
4
5
6
7

8
9

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

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001012080200
title: API: Check for authentication
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220103224858
modified: 20241216104549
modified: 20220908163156

API clients typically want to know, whether [[authentication is enabled|00001010040100]] or not.
API clients typically wants to know, whether [[authentication is enabled|00001010040100]] or not.
If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication.
Then they try to [[obtain an access token|00001012050200]].
If authentication is disabled, these steps are not needed.

To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''.

```sh

Changes to docs/manual/00001012080500.zettel.

1
2
3
4
5
6
7

8
9
10
11
12

13
14
15
16
17
18

19
20
21
22
23
24
25
1
2
3
4
5
6

7
8
9
10
11

12
13
14
15
16
17

18
19
20
21
22
23
24
25






-
+




-
+





-
+







id: 00001012080500
title: API: Refresh internal data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211230230441
modified: 20250102212703
modified: 20220923104836

Zettelstore maintains some internal data to allow faster operations.

One example is the [[content search|00001012051400]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term.
Instead, all words are stored internally, with a list of zettel where they occur.
Instead, all word are stored internally, with a list of zettel where they occur.

Another example is the way to determine which zettel are stored in a [[ZIP file|00001004011200]].
Scanning a ZIP file is a lengthy operation, therefore Zettelstore maintains a directory of zettel for each ZIP file.

All these internal data may become stale.
This should not happen, but when it comes e.g. to file handling, every operating system behaves differently in very subtle ways.
This should not happen, but when it comes e.g. to file handling, every operating systems behaves differently in very subtle ways.

To avoid stopping and re-starting Zettelstore, you can use the API to force Zettelstore to refresh its internal data if you think it is needed.
To do this, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=refresh''.

```sh
# curl -X POST 'http://127.0.0.1:23123/x?cmd=refresh'
```

Changes to docs/manual/00001012920516.zettel.

1
2
3
4
5
6
7

8
9
10
11
12


13
14
15
16
17
18

19
20
1
2
3
4
5
6

7
8
9
10


11
12
13
14
15
16
17

18
19
20






-
+



-
-
+
+





-
+


id: 00001012920516
title: Sz Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220422181104
modified: 20250102214350
modified: 20230403161458

A zettel representation that is a [[s-expression|00001012930000]] (also known as symbolic expression).

It is (relatively) easy to parse and contains all relevant information of metadata, content or the whole zettel.
For example, take a look at the Sz encoding of this page, which is available on the ""Info"" sub-page of this zettel: 
It is (relatively) easy to parse and contain all relevant information of a zettel, metadata and content.
For example, take a look at the Sz encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920516?enc=sz&part=zettel]],
* [[//z/00001012920516?enc=sz&part=meta]],
* [[//z/00001012920516?enc=sz&part=content]].

Some zettel provide a more detailed description of the [[Sz encoding|00001012931000]].
Some zettel describe the [[Sz encoding|00001012931000]] in a more detailed way.

If transferred via HTTP, the content type will be ''text/plain''.

Changes to docs/manual/00001012920525.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012920525
title: SHTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230316181044
modified: 20250102180003
modified: 20230403150657

A zettel representation that is a [[s-expression|00001012930000]], syntactically similar to the [[Sz encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics.
It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]].

It is (relatively) easy to parse and contains everything to transform it into real HTML.
In contrast to HTML, SHTML is easier to parse and to manipulate.
For example, take a look at the SHTML encoding of this page, which is available via the ""Info"" sub-page of this zettel: 
22
23
24
25
26
27
28
29

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

29
30
31
32
33
34
35
36







-
+







Internally, if a zettel should be transformed into HTML, the zettel is translated into the [[Sz encoding|00001012920516]], which is transformed into this SHTML encoding to produce the [[HTML encoding|00001012920510]].

=== Syntax of SHTML
There are only two types of elements: atoms and lists, similar to the Sz encoding.

A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.
Before the last element of a list of at least two elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements.
Before the last element of a list of at least to elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements.
This allows a more space economic storage of data.

An HTML tag like ``< a href="link">Text</a>`` is encoded in SHTML with a list, where the first element is a symbol named a the tag.
The second element is an optional encoding of the tag's attributes.
Further elements are either other tag encodings or a string.
The above tag is encoded as ``(a (@ (href . "link")) "Text")``.
Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character).

Changes to docs/manual/00001012930500.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012930500
title: Syntax of Symbolic Expressions
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20230403151127
modified: 20250102175559
modified: 20240413160345

=== Syntax of lists
A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.

Internally, lists are composed of __cells__.
A cell allows to store two values.
53
54
55
56
57
58
59
60

61
62
63

64
65
66

67
68
69
70



71
72
73
74
75
76
77

53
54
55
56
57
58
59

60
61
62

63
64
65

66
67



68
69
70
71
72
73
74
75
76

77







-
+


-
+


-
+

-
-
-
+
+
+






-
+

=== Syntax of numbers (atom)
A number is a non-empty sequence of digits (""0"" ... ""9"").
The smallest number is ``0``, there are no negative numbers.

=== Syntax of symbols (atom)
A symbol is a non-empty sequence of printable characters, except left or right parenthesis.
Unicode characters of the following categories contain printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S).
Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S).
Symbols are case-sensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote different symbols.

=== Syntax of strings (atom)
=== Syntax of string (atom)

A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark.
To allow a string to contain a quotation mark, it must be prefixed by one backslash (""''\\''"", U+005C).
To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C).
To allow a string to contain a backslash, it also must be prefixed by one backslash.
Unicode characters with a code less than U+FF are encoded by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFF are encoded by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFFFF are encoded by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character.
Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character.
In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A).

=== See also
* Currently, Zettelstore uses [[Sx|https://t73f.de/r/sx]] (""Symbolic eXpression framework"") to implement symbolic expressions.
  The project page might contain additional information about the full syntax.

  Zettelstore only uses lists, numbers, strings, and symbols to represent zettel.
  Zettelstore only uses lists, numbers, string, and symbols to represent zettel.

Changes to docs/manual/00001012931000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16
17
18
19
20
1
2
3
4
5
6

7
8
9
10
11
12

13
14
15
16
17
18
19
20






-
+





-
+







id: 00001012931000
title: Encoding of Sz
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403153903
modified: 20250102213403
modified: 20240123120319

Zettel in a [[Sz encoding|00001012920516]] are represented as a [[symbolic expression|00001012930000]].
To process these symbolic expressions, you need to know, how a specific part of a zettel is represented by a symbolic expression.

Basically, each part of a zettel is represented as a list, often a nested list.
The first element of that list is always a unique symbol, which denotes that part.
The first element of that list is always an unique symbol, which denotes that part.
The meaning / semantic of all other elements depend on that symbol.

=== Zettel
A full zettel is represented by a list of two elements.
The first elements represents the metadata, the second element represents the zettel content.

:::syntax

Changes to docs/manual/00001012931200.zettel.

1
2
3
4
5
6
7

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

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

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






-
+



















+








id: 00001012931200
title: Encoding of Sz Metadata
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161618
modified: 20250115172406
modified: 20240219161848

A single metadata (""metadatum"") is represented by a triple: a symbol representing the type, a symbol representing the key, and either a string or a list that represent the value.

The symbol depends on the [[metadata key type|00001006030000]].
The value also depends somehow on the key type: a set of values is represented as a list, all other values are represented by a string, even if it is a number.

The following table maps key types to symbols and to the type of the value representation.

|=Key Type<| Symbol<| Value<
| [[Credential|00001006031000]] | ''CREDENTIAL'' | string
| [[EString|00001006031500]] | ''EMPTY-STRING'' | string
| [[Identifier|00001006032000]] | ''ZID'' | string
| [[IdentifierSet|00001006032500]] | ''ZID-SET'' | ListValue
| [[Number|00001006033000]] | ''NUMBER'' | string
| [[String|00001006033500]] | ''STRING'' | string
| [[TagSet|00001006034000]] | ''TAG-SET'' | ListValue
| [[Timestamp|00001006034500]] | ''TIMESTAMP'' | string
| [[URL|00001006035000]] | ''URL'' | string
| [[Word|00001006035500]] | ''WORD'' | string
| [[Zettelmarkup|00001006036500]] | ''ZETTELMARKUP'' | string

:::syntax
__ListValue__ **=** ''('' String,,1,, String,,2,, &hellip; String,,n,, '')''.
:::

Examples:
* The title of this zettel is represented as: ''(EMPTY-STRING title "Encoding of Sz Metadata")''
* The tags of this zettel are represented as: ''(TAG-SET tags ("#api" "#manual" "#reference" "#zettelstore"))''

Changes to docs/manual/00001012931400.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012931400
title: Encoding of Sz Block Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161803
modified: 20241216104739
modified: 20240123120132

=== ''PARA''
:::syntax
__Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A paragraph is just a list of inline elements.

43
44
45
46
47
48
49
50

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

50
51
52
53
54
55
56
57







-
+







:::
A list element is either a block or an inline.
If it is a block, it may contain a nested list.
=== ''DESCRIPTION''
:::syntax
__Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ &hellip; '')''.
:::
A description is a sequence of one or more terms and values.
A description is a sequence of one ore more terms and values.

:::syntax
__DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A description term is just an inline-structured value.

:::syntax
122
123
124
125
126
127
128
129

130
131
132
133
134
135
136
122
123
124
125
126
127
128

129
130
131
132
133
134
135
136







-
+







Attributes may further specify the quotation.
The inline typically describes author / source of the quotation.

:::syntax
__VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] &hellip; '')'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A block region just treats the block to contain a verse.
Soft line breaks are transformed into hard line breaks to save the structure of the verse / poem.
Soft line break are transformed into hard line breaks to save the structure of the verse / poem.
Attributes may further specify something.
The inline typically describes author / source of the verse.

=== ''VERBATIM-*''
The following lists specifies some literal text of more than one line.
The structure is always the same, the initial symbol denotes the actual usage.
The content is encoded as a string, most likely to contain control characters that signals the end of a line.

Changes to docs/manual/00001012931600.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20250102221540
modified: 20240620170546

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, including white space characters.

92
93
94
95
96
97
98
99

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

99
100
101
102
103
104
105
106







-
+







=== ''EMBED-BLOB''
:::syntax
__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''.
:::
If used if some processed image has to be embedded inside some inline material.
The first string specifies the syntax of the image content.
The second string contains the image content.
If the syntax is ""SVG"", the image content is not encoded further.
If the syntax is ""SVG"", the image content is not further encoded.
Otherwise a base64 encoding is used.

=== ''CITE''
:::syntax
__CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the citation key.
138
139
140
141
142
143
144
145

146
147
148
149
150
151
152
138
139
140
141
142
143
144

145
146
147
148
149
150
151
152







-
+







__InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as inserted.

:::syntax
__MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as highlighted for the reader (but was not important to the original author).
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
187
188
189
190
191
192
193
194

195
196
197
198
199
200
201
202
203
204

205
206
207
208
209
187
188
189
190
191
192
193

194
195
196
197
198
199
200
201
202
203

204
205
206
207
208
209







-
+









-
+





__HTMLLiteral__ **=** ''(LITERAL-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as HTML code.

:::syntax
__InputLiteral__ **=** ''(LITERAL-INPUT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as input entered by a user.
The string contains text that should be treated as input entered by an user.

:::syntax
__MathLiteral__ **=** ''(LITERAL-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as special code to be interpreted as mathematical formulas.

:::syntax
__OutputLiteral__ **=** ''(LITERAL-OUTPUT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as computer output to be read by a user.
The string contains text that should be treated as computer output to be read by an user.

:::syntax
__ZettelLiteral__ **=** ''(LITERAL-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (nested) zettel content.

Changes to docs/manual/00001012931900.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012931900
title: Encoding of Sz Reference Values
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230405123046
modified: 20250102172048
modified: 20240122094720

A reference is encoded as the actual reference value, and a symbol describing the state of that actual reference value.

:::syntax
__Reference__ **=** ''('' __ReferenceState__ String '')''.
:::
The string contains the actual reference value.
26
27
28
29
30
31
32
33

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

33
34
35
36
37
38
39
40
41
42







-
+









  This value is only possible before evaluating the zettel.
; ''SELF''
: The reference value is a reference to the same zettel, to a specific mark.
; ''FOUND''
: The reference value is a valid reference to an existing zettel.
  This value is only possible after evaluating the zettel.
; ''BROKEN''
: The reference value is a valid reference to a missing zettel.
: The reference value is a valid reference to an missing zettel.
  This value is only possible after evaluating the zettel.
; ''HOSTED''
: The reference value starts with one slash character, denoting an absolute local reference.
; ''BASED''
: The reference value starts with two slash characters, denoting a local reference interpreted relative to the Zettelstore base URL.
; ''QUERY''
: The reference value contains a query expression.
; ''EXTERNAL''
: The reference value contains a full URL, referencing a resource outside of the Zettelstore server.

Changes to docs/manual/00001017000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12

13
14
15
16
17
18

19
20
21
22
23
24
25
1
2
3
4
5
6

7
8
9
10
11

12
13
14
15
16
17

18
19
20
21
22
23
24
25






-
+




-
+





-
+







id: 00001017000000
title: Tips and Tricks
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20220803170112
modified: 20250102190553
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 specifying one [[''home-zettel''|00001004020000#home-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.
   Let's assume this zettel receives the zettel identifier ''20220803182600''.
*# Create the zettel that should serve as the starting zettel for your users.
   It must have syntax [[Zettelmarkup|00001008000000#zmk]], i.e. the syntax metadata must be set to ''zmk''.
   If needed, set the runtime configuration [[''home-zettel''|00001004020000#home-zettel]] to the value of the identifier of this zettel.
   If needed, set the runtime configuration [[''home-zettel|00001004020000#home-zettel]] to the value of the identifier of this zettel.
*# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel).
* **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user.
  In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel.
* **Solution 2:** Set a user-specific value by adding metadata ''home-zettel'' to the [[user zettel|00001010040200]].
* **Discussion:** A value for ''home-zettel'' is first searched in the user zettel of the current authenticated user.
  Only if it is not found, the value is looked up in the runtime configuration zettel.
  If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel.
44
45
46
47
48
49
50
51

52
53
54

55
56
57
58
59
60

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

73
74
75
76

77
78
79
80
44
45
46
47
48
49
50

51
52
53

54
55
56
57
58
59

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

72
73
74
75

76
77
78
79
80







-
+


-
+





-
+











-
+



-
+





  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 same set of zettel across all computers.
* **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 your directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number).
  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''.
  The ""''~''"" is a shortcut and specifies your home folder.

  Unfortunately, Zettelstore does not yet support this shortcut.
  Therefore you must replace it with the absolute name of your home folder.
  In addition, a space character is not allowed in a URI.
  In addition, a space character is not allowed in an URI.
  You have to replace it with the sequence ""''%20''"".

  Let us assume, that you stored your zettel box inside the folder ""zettel"", which is located top-level in your iCloud folder.
  In this case, you must specify the following box URI within the startup configuration: ''box-uri-1: dir:///Users/USERNAME/Library/Mobile%20Documents/com~apple~CloudDocs/zettel'', replacing ''USERNAME'' with the username of that specific computer (and assuming you want to use it as the first box).
* **Solution 2:** If you typically start your Zettelstore on the command line, you could use the ''-d DIR'' option for the [[''run''|00001004051000#d]] sub-command.
  In this case you are allowed to use the character ""''~''"".

  ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel''

  (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.)
* **Discussion:** Zettel files are synchronized between your computers via iCloud.
  It does not matter, if one of your computers is offline or switched off.
  Is does not matter, if one of your computer is offline / switched off.
  iCloud will synchronize the zettel files if it later comes online.

  However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time.
  It might take several seconds, maybe longer, that the new version of a zettel appears on the other computer.
  It might take several seconds, maybe longer, that new new version of a zettel appears on the other computer.
  If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner.
  Zettelstore is intentionally not aware of any synchronization within its zettel boxes.

  If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information.

Changes to docs/manual/00001018000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26

27
28
29
30

31
32
33
34

35
36
37
38
39
40
41
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24
25

26
27
28
29

30
31
32
33

34
35
36
37
38
39
40
41






-
+








-
+









-
+



-
+



-
+







id: 00001018000000
title: Troubleshooting
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211027105921
modified: 20250106180049
modified: 20241212153148

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

=== Installation
* **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer.
  Therefore, it will not start Zettelstore.
** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click.
   A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore.
   This dialog is only presented once for a given Zettelstore executable.
   This dialog is only resented once for a given Zettelstore executable.
* **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer.
** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allow to start Zettelstore.

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

=== Working with Zettel Files
* **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes does not detect the change.
* **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes did not detect that change.
  If you access the zettel via Zettelstore, an error is reported.
** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel.
   This occurs mostly under MacOS.
** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you can find in the menu ""Lists"").
** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you find it in the menu ""Lists"").
** **Solution 2:** There is an [[API|00001012080500]] call to make Zettelstore aware of this change.
** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible.
** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]].

=== HTML content is not shown
* **Problem:** You have entered some HTML code as content for your Zettelstore, but this content is not shown on the Web User Interface.

Changes to encoder/encoder.go.

17
18
19
20
21
22
23
24
25


26
27
28
29
30
31


32
33
34
35




36
37
38
39
40
41
42
17
18
19
20
21
22
23


24
25
26
27
28
29


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







-
-
+
+




-
-
+
+




+
+
+
+








import (
	"errors"
	"fmt"
	"io"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/zettel/meta"
)

// Encoder is an interface that allows to encode different parts of a zettel.
type Encoder interface {
	WriteZettel(io.Writer, *ast.ZettelNode) (int, error)
	WriteMeta(io.Writer, *meta.Meta) (int, error)
	WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error)
	WriteMeta(io.Writer, *meta.Meta, EvalMetaFunc) (int, error)
	WriteContent(io.Writer, *ast.ZettelNode) (int, error)
	WriteBlocks(io.Writer, *ast.BlockSlice) (int, error)
	WriteInlines(io.Writer, *ast.InlineSlice) (int, error)
}

// EvalMetaFunc is a function that takes a string of metadata and returns
// a list of syntax elements.
type EvalMetaFunc func(string) ast.InlineSlice

// Some errors to signal when encoder methods are not implemented.
var (
	ErrNoWriteZettel  = errors.New("method WriteZettel is not implemented")
	ErrNoWriteMeta    = errors.New("method WriteMeta is not implemented")
	ErrNoWriteContent = errors.New("method WriteContent is not implemented")
	ErrNoWriteBlocks  = errors.New("method WriteBlocks is not implemented")

Changes to encoder/encoder_blob_test.go.

12
13
14
15
16
17
18
19

20
21
22
23


24
25
26
27
28
29
30
12
13
14
15
16
17
18

19

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







-
+
-



+
+







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

package encoder_test

import (
	"testing"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser.
)

type blobTestCase struct {
	descr  string
	blob   []byte
49
50
51
52
53
54
55
56

57
58
59
60


61
62
50
51
52
53
54
55
56

57
58
59


60
61
62
63







-
+


-
-
+
+


			encoderZmk:   `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`,
		},
	},
}

func TestBlob(t *testing.T) {
	m := meta.New(id.Invalid)
	m.Set(meta.KeyTitle, "PNG")
	m.Set(api.KeyTitle, "PNG")
	for testNum, tc := range pngTestCases {
		inp := input.NewInput(tc.blob)
		bs := parser.ParseBlocks(inp, m, "png", config.NoHTML)
		checkEncodings(t, testNum, bs, false, tc.descr, tc.expect, "???")
		pe := &peBlocks{bs: parser.ParseBlocks(inp, m, "png", config.NoHTML)}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, "???")
	}
}

Changes to encoder/encoder_inline_test.go.

633
634
635
636
637
638
639
























640
641
642
643
644
645
646
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670







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







			encoderMD:    "![abc](abc)",
			encoderSz:    `(INLINE (EMBED () (EXTERNAL "abc") ""))`,
			encoderSHTML: `((img (@ (src . "abc"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Inline HTML Zettel",
		zmk:   `@@<hr>@@{="html"}`,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(INLINE)`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   ``,
		},
	},
	{
		descr: "Inline Text Zettel",
		zmk:   `@@<hr>@@{="text"}`,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "<hr>",
			encoderSz:    `(INLINE (LITERAL-ZETTEL (("" . "text")) "<hr>"))`,
			encoderSHTML: `(())`,
			encoderText:  `<hr>`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(INLINE)`,

Changes to encoder/encoder_test.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
16
17
18
19
20
21
22

23
24
25
26
27
28
29






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







-





+

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







import (
	"fmt"
	"strings"
	"testing"

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/encoder/htmlenc"   // Allow to use HTML encoder.
	_ "zettelstore.de/z/encoder/mdenc"     // Allow to use markdown encoder.
	_ "zettelstore.de/z/encoder/shtmlenc"  // Allow to use SHTML encoder.
	_ "zettelstore.de/z/encoder/szenc"     // Allow to use sz encoder.
	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
	_ "zettelstore.de/z/encoder/htmlenc"  // Allow to use HTML encoder.
	_ "zettelstore.de/z/encoder/mdenc"    // Allow to use markdown encoder.
	_ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder.
	_ "zettelstore.de/z/encoder/szenc"    // Allow to use sz encoder.
	_ "zettelstore.de/z/encoder/textenc"  // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zmkenc"   // Allow to use zmk encoder.
	"zettelstore.de/z/parser/cleaner"
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

type zmkTestCase struct {
	descr  string
	zmk    string
	inline bool
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




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







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



-
+

-
-
+
+





-
+











-
+





-
+


-
+















-
+




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


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

-
-
+
+
+
+
	}
	executeTestCases(t, append(tcsBlock, tcsInline...))
}

func executeTestCases(t *testing.T, testCases []zmkTestCase) {
	for testNum, tc := range testCases {
		inp := input.NewInput([]byte(tc.zmk))
		var pe parserEncoder
		if tc.inline {
			is := parser.ParseInlines(inp, meta.SyntaxZmk)
			cleaner.CleanInlineSlice(&is)
			pe = &peInlines{is: is}
		} else {
		bs := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)
		checkEncodings(t, testNum, bs, tc.inline, tc.descr, tc.expect, tc.zmk)
		checkSz(t, testNum, bs, tc.inline, tc.descr)
			pe = &peBlocks{bs: parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML)}
		}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk)
		checkSz(t, testNum, pe, tc.descr)
	}
}

func checkEncodings(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string, expected expectMap, zmkDefault string) {
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: meta.ValueLangEN})
		got, err := encode(encdr, bs, isInline)
		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:     " + mode(isInline)
			prefix += "\nMode:     " + pe.mode()
			t.Errorf("%s\nEncoder:  %s\nError:    %v", prefix, enc, err)
			continue
		}
		if enc == api.EncoderZmk && exp == useZmk {
			exp = zmkDefault
		}
		if got != exp {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + mode(isInline)
			prefix += "\nMode:     " + pe.mode()
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}

func checkSz(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string) {
func checkSz(t *testing.T, testNum int, pe parserEncoder, descr string) {
	t.Helper()
	encdr := encoder.Create(encoderSz, nil)
	exp, err := encode(encdr, bs, isInline)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxreader.MakeReader(strings.NewReader(exp)).Read()
	if err != nil {
		t.Error(err)
		return
	}
	got := val.String()
	if exp != got {
		prefix := fmt.Sprintf("Test #%d", testNum)
		if d := descr; d != "" {
			prefix += "\nReason:   " + d
		}
		prefix += "\nMode:     " + mode(isInline)
		prefix += "\nMode:     " + pe.mode()
		t.Errorf("%s\n\nExpected: %q\nGot:      %q", prefix, exp, got)
	}
}

type parserEncoder interface {
func encode(e encoder.Encoder, bs ast.BlockSlice, isInline bool) (string, error) {
	var sb strings.Builder
	encode(encoder.Encoder) (string, error)
	mode() string
	if !isInline {
		_, err := e.WriteBlocks(&sb, &bs)
		return sb.String(), err
	}
	var is ast.InlineSlice
}

type peInlines struct {
	is ast.InlineSlice
	if len(bs) > 0 {
		if pn, ok := bs[0].(*ast.ParaNode); ok {
			is = pn.Inlines
		}
	}
	_, err := e.WriteInlines(&sb, &is)
	return sb.String(), err
}

func (in peInlines) encode(encdr encoder.Encoder) (string, error) {
	var sb strings.Builder
	if _, err := encdr.WriteInlines(&sb, &in.is); err != nil {
		return "", err
	}
	return sb.String(), nil
}

func (peInlines) mode() string { return "inline" }
func mode(isInline bool) string {
	if isInline {
		return "inline"

type peBlocks struct {
	bs ast.BlockSlice
}

func (bl peBlocks) encode(encdr encoder.Encoder) (string, error) {
	var sb strings.Builder
	if _, err := encdr.WriteBlocks(&sb, &bl.bs); err != nil {
		return "", err
	}
	return "block"
}
	return sb.String(), nil

}
func (peBlocks) mode() string { return "block" }

Changes to encoder/htmlenc/htmlenc.go.

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

31
32
33
34
35
36
37
17
18
19
20
21
22
23

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







-






+







import (
	"io"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(
		api.EncoderHTML,
		func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) },
	)
54
55
56
57
58
59
60
61

62
63

64
65
66
67
68
69
70

71
72

73
74
75
76
77
78
79
54
55
56
57
58
59
60

61
62

63
64
65
66
67
68
69

70
71

72
73
74
75
76
77
78
79







-
+

-
+






-
+

-
+







	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) (int, error) {
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), &env)
	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(meta.KeyTitle)
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		isTitle = parser.ParseSpacedText(string(plainTitle))
		isTitle = parser.ParseSpacedText(plainTitle)
		xtitle := he.tx.GetSz(&isTitle)
		htitle, err = he.th.Evaluate(xtitle, &env)
		if err != nil {
			return 0, err
		}
	}

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







-
+







-
+

-
+







	if hen != nil {
		body.Add(sx.Cons(shtml.SymHR, nil))
		body.Add(hen)
	}

	doc := sx.MakeList(
		sxhtml.SymDoctype,
		sx.MakeList(shtml.SymHTML, head.List(), body.List()),
		sx.MakeList(shtml.SymHtml, head.List(), body.List()),
	)

	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
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), &env)
	hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteListHTML(w, hm)
}

Changes to encoder/mdenc/mdenc.go.

15
16
17
18
19
20
21
22
23
24
25

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

47
48

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

61
62

63
64
65
66
67
68
69











70
71
72
73
74
75
76
15
16
17
18
19
20
21

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

46
47

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

60
61

62
63
64
65
66



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







-



+




















-
+

-
+











-
+

-
+




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







package mdenc

import (
	"io"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(
		api.EncoderMD,
		func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) },
	)
}

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

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

// WriteZettel writes the encoded zettel to the writer.
func (me *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (me *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, me.lang)
	v.acceptMeta(zn.InhMeta)
	v.acceptMeta(zn.InhMeta, evalMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteByte('\n')
	}
	ast.Walk(v, &zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.
func (me *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
func (me *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, me.lang)
	v.acceptMeta(m)
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (v *visitor) acceptMeta(m *meta.Meta) {
	for key, val := range m.Computed() {
		v.b.WriteStrings(key, ": ", string(val), "\n")
func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
		key := p.Key
		v.b.WriteStrings(key, ": ")
		if meta.Type(key) == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			ast.Walk(v, &is)
		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}

// WriteContent encodes the zettel content.
func (me *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return me.WriteBlocks(w, &zn.Ast)
}
170
171
172
173
174
175
176
177

178
179
180
181
182
183
184
178
179
180
181
182
183
184

185
186
187
188
189
190
191
192







-
+







		}
		ast.Walk(v, bn)
	}
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	lc := len(vn.Content)
	if vn.Kind != ast.VerbatimCode || lc == 0 {
	if vn.Kind != ast.VerbatimProg || lc == 0 {
		return
	}
	v.writeSpaces(4)
	lcm1 := lc - 1
	for i := 0; i < lc; i++ {
		b := vn.Content[i]
		if b != '\n' && b != '\r' {
387
388
389
390
391
392
393
394

395
396
397
398

399
400
401
402
403
404
405
406
407
408
395
396
397
398
399
400
401

402
403
404
405

406
407
408
409
410
411
412
413
414
415
416







-
+



-
+










		v.b.WriteString("&nbsp;")
	}
	v.b.WriteString(rightQ)
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput:
	case ast.LiteralProg, ast.LiteralInput, ast.LiteralOutput:
		v.b.WriteByte('`')
		v.b.Write(ln.Content)
		v.b.WriteByte('`')
	case ast.LiteralComment: // ignore everything
	case ast.LiteralComment, ast.LiteralHTML: // ignore everything
	default:
		v.b.Write(ln.Content)
	}
}

func (v *visitor) writeSpaces(n int) {
	for range n {
		v.b.WriteByte(' ')
	}
}

Changes to encoder/shtmlenc/shtmlenc.go.

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

27
28
29
30
31
32
33
15
16
17
18
19
20
21

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







-




+







package shtmlenc

import (
	"io"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) })
}

// Create a SHTML encoder
45
46
47
48
49
50
51
52

53
54

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

68
69

70
71
72
73
74
75
76
45
46
47
48
49
50
51

52
53

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

67
68

69
70
71
72
73
74
75
76







-
+

-
+












-
+

-
+







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) (int, error) {
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), &env)
	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) (int, error) {
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), &env)
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, metaSHTML)
}

// WriteContent encodes the zettel content.

Changes to encoder/szenc/szenc.go.

15
16
17
18
19
20
21
22
23
24

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

45
46

47
48
49
50
51
52


53
54
55
56
57
58
59
15
16
17
18
19
20
21

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

44
45

46
47
48
49
50


51
52
53
54
55
56
57
58
59







-


+



















-
+

-
+




-
-
+
+







package szenc

import (
	"io"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSz, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create a S-expr encoder
func Create() *Encoder {
	// We need a new transformer every time, because trans.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{trans: NewTransformer()}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	trans *Transformer
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	content := enc.trans.GetSz(&zn.Ast)
	meta := enc.trans.GetMeta(zn.InhMeta)
	meta := enc.trans.GetMeta(zn.InhMeta, evalMeta)
	return sx.MakeList(meta, content).Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	return enc.trans.GetMeta(m).Print(w)
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	return enc.trans.GetMeta(m, evalMeta).Print(w)
}

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

Changes to encoder/szenc/transform.go.

16
17
18
19
20
21
22
23
24
25


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

23
24
25
26
27
28
29
30
31
32
33







-


+
+







import (
	"encoding/base64"
	"fmt"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	t := Transformer{}
	return &t
}
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
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







-
+



















+
-
+



+







		)
	}
	return sx.MakeList(sz.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node)))
}

var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
	ast.VerbatimZettel:  sz.SymVerbatimZettel,
	ast.VerbatimCode:    sz.SymVerbatimCode,
	ast.VerbatimProg:    sz.SymVerbatimProg,
	ast.VerbatimEval:    sz.SymVerbatimEval,
	ast.VerbatimMath:    sz.SymVerbatimMath,
	ast.VerbatimComment: sz.SymVerbatimComment,
	ast.VerbatimHTML:    sz.SymVerbatimHTML,
}

var mapFormatKindS = map[ast.FormatKind]*sx.Symbol{
	ast.FormatEmph:   sz.SymFormatEmph,
	ast.FormatStrong: sz.SymFormatStrong,
	ast.FormatDelete: sz.SymFormatDelete,
	ast.FormatInsert: sz.SymFormatInsert,
	ast.FormatSuper:  sz.SymFormatSuper,
	ast.FormatSub:    sz.SymFormatSub,
	ast.FormatQuote:  sz.SymFormatQuote,
	ast.FormatMark:   sz.SymFormatMark,
	ast.FormatSpan:   sz.SymFormatSpan,
}

var mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{
	ast.LiteralZettel:  sz.SymLiteralZettel,
	ast.LiteralCode:    sz.SymLiteralCode,
	ast.LiteralProg:    sz.SymLiteralProg,
	ast.LiteralInput:   sz.SymLiteralInput,
	ast.LiteralOutput:  sz.SymLiteralOutput,
	ast.LiteralComment: sz.SymLiteralComment,
	ast.LiteralHTML:    sz.SymLiteralHTML,
	ast.LiteralMath:    sz.SymLiteralMath,
}

var mapRegionKindS = map[ast.RegionKind]*sx.Symbol{
	ast.RegionSpan:  sz.SymRegionBlock,
	ast.RegionQuote: sz.SymRegionQuote,
	ast.RegionVerse: sz.SymRegionVerse,
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
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







-
+
+

-
+








-
+

-
+







			}
		}
	}
	return true
}

func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair {
	dlObjs := make(sx.Vector, 2*len(dn.Descriptions))
	dlObjs := make(sx.Vector, 2*len(dn.Descriptions)+1)
	dlObjs[0] = sz.SymDescription
	for i, def := range dn.Descriptions {
		dlObjs[2*i] = t.getInlineList(def.Term)
		dlObjs[2*i+1] = t.getInlineList(def.Term)
		descObjs := make(sx.Vector, len(def.Descriptions))
		for j, b := range def.Descriptions {
			dVal := make(sx.Vector, len(b))
			for k, dn := range b {
				dVal[k] = t.GetSz(dn)
			}
			descObjs[j] = sx.MakeList(dVal...).Cons(sz.SymBlock)
		}
		dlObjs[2*i+1] = sx.MakeList(descObjs...).Cons(sz.SymBlock)
		dlObjs[2*i+2] = sx.MakeList(descObjs...).Cons(sz.SymBlock)
	}
	return sx.MakeList(dlObjs...).Cons(sz.SymDescription)
	return sx.MakeList(dlObjs...)
}

func (t *Transformer) getTable(tn *ast.TableNode) *sx.Pair {
	tObjs := make(sx.Vector, len(tn.Rows)+2)
	tObjs[0] = sz.SymTable
	tObjs[1] = t.getHeader(tn.Header)
	for i, row := range tn.Rows {
256
257
258
259
260
261
262
263

264
265
266
267
268
269
270
260
261
262
263
264
265
266

267
268
269
270
271
272
273
274







-
+








func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
	return t.getInlineList(cell.Inlines).Cons(mapGetS(alignmentSymbolS, cell.Align))
}

func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
	var lastObj sx.Object
	if bn.Syntax == meta.ValueSyntaxSVG {
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sx.MakeString(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sx.MakeList(
		sz.SymBLOB,
		t.getInlineList(bn.Description),
290
291
292
293
294
295
296
297

298
299
300
301
302
303
304
294
295
296
297
298
299
300

301
302
303
304
305
306
307
308







-
+







		Cons(sx.MakeString(ln.Ref.Value)).
		Cons(getAttributes(ln.Attrs)).
		Cons(mapGetS(mapRefStateLink, ln.Ref.State))
}

func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
	tail := t.getInlineList(en.Inlines)
	if en.Syntax == meta.ValueSyntaxSVG {
	if en.Syntax == meta.SyntaxSVG {
		tail = tail.Cons(sx.MakeString(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return tail.Cons(sx.MakeString(en.Syntax)).Cons(getAttributes(en.Attrs)).Cons(sz.SymEmbedBLOB)
}

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
346
347
348
349
350
351
352










353
354
355
356
357
358
359
360
361
362
363
364
365
366



367
368
369
370
371
372
373
374
375

376
377
378
379
380
381
382
383
384
385

386
387
388
389
390
391
392
393







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



-
-
-
+
+
+
+
+




-
+





+
+
+

-
+







}

func getReference(ref *ast.Reference) *sx.Pair {
	return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.MakeString(ref.Value))
}

var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
	meta.TypeCredential: sz.SymTypeCredential,
	meta.TypeEmpty:      sz.SymTypeEmpty,
	meta.TypeID:         sz.SymTypeID,
	meta.TypeIDSet:      sz.SymTypeIDSet,
	meta.TypeNumber:     sz.SymTypeNumber,
	meta.TypeString:     sz.SymTypeString,
	meta.TypeTagSet:     sz.SymTypeTagSet,
	meta.TypeTimestamp:  sz.SymTypeTimestamp,
	meta.TypeURL:        sz.SymTypeURL,
	meta.TypeWord:       sz.SymTypeWord,
	meta.TypeCredential:   sz.SymTypeCredential,
	meta.TypeEmpty:        sz.SymTypeEmpty,
	meta.TypeID:           sz.SymTypeID,
	meta.TypeIDSet:        sz.SymTypeIDSet,
	meta.TypeNumber:       sz.SymTypeNumber,
	meta.TypeString:       sz.SymTypeString,
	meta.TypeTagSet:       sz.SymTypeTagSet,
	meta.TypeTimestamp:    sz.SymTypeTimestamp,
	meta.TypeURL:          sz.SymTypeURL,
	meta.TypeWord:         sz.SymTypeWord,
	meta.TypeZettelmarkup: sz.SymTypeZettelmarkup,
}

// GetMeta transforms the given metadata into a sx list.
func (t *Transformer) GetMeta(m *meta.Meta) *sx.Pair {
	objs := sx.Vector{}
	for key, val := range m.Computed() {
func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	pairs := m.ComputedPairs()
	objs := make(sx.Vector, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			setList := val.AsSlice()
			setList := meta.ListFromValue(p.Value)
			setObjs := make(sx.Vector, len(setList))
			for i, val := range setList {
				setObjs[i] = sx.MakeString(val)
			}
			obj = sx.MakeList(setObjs...)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			obj = t.getInlineList(is)
		} else {
			obj = sx.MakeString(string(val))
			obj = sx.MakeString(p.Value)
		}
		objs = append(objs, sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType))
	}
	return sx.MakeList(objs...).Cons(sz.SymMeta)
}

func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol {

Changes to encoder/textenc/textenc.go.

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

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

42
43

44
45
46
47
48
49
50

51
52
53
54
55
56









57
58
59
60
61
62
63
64

65
66
67


68
69
70
71

72
73
74
75
76
77
78
12
13
14
15
16
17
18

19
20

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

40
41

42
43
44
45
46
47
48

49
50





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

67



68
69
70
71


72
73
74
75
76
77
78
79







-


-



+















-
+

-
+






-
+

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







-
+
-
-
-
+
+


-
-
+







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

// Package textenc encodes the abstract syntax tree into its text.
package textenc

import (
	"io"
	"iter"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myTE }

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

var myTE Encoder // Only a singleton is required.

// WriteZettel writes metadata and content.
func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	te.WriteMeta(&v.b, zn.InhMeta)
	te.WriteMeta(&v.b, zn.InhMeta, evalMeta)
	v.visitBlockSlice(&zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes metadata as text.
func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	buf := encoder.NewEncWriter(w)
	for key, val := range m.Computed() {
		if meta.Type(key) == meta.TypeTagSet {
			writeTagSet(&buf, val.Elems())
		} else {
			buf.WriteString(string(val))
	for _, pair := range m.ComputedPairs() {
		switch meta.Type(pair.Key) {
		case meta.TypeTagSet:
			writeTagSet(&buf, meta.ListFromValue(pair.Value))
		case meta.TypeZettelmarkup:
			is := evalMeta(pair.Value)
			te.WriteInlines(&buf, &is)
		default:
			buf.WriteString(pair.Value)
		}
		buf.WriteByte('\n')
	}
	length, err := buf.Flush()
	return length, err
}

func writeTagSet(buf *encoder.EncWriter, tags iter.Seq[meta.Value]) {
func writeTagSet(buf *encoder.EncWriter, tags []string) {
	first := true
	for tag := range tags {
		if !first {
	for i, tag := range tags {
		if i > 0 {
			buf.WriteByte(' ')
		}
		first = false
		buf.WriteString(string(tag.CleanTag()))
		buf.WriteString(meta.CleanTag(tag))
	}

}

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

Changes to encoder/zmkenc/zmkenc.go.

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


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

45
46

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

59
60

61
62
63
64
65
66
67











68
69
70
71
72
73
74
15
16
17
18
19
20
21

22
23

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

44
45

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

58
59

60
61
62
63
64



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







-


-



+
+















-
+

-
+











-
+

-
+




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







package zmkenc

import (
	"fmt"
	"io"
	"strings"

	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myZE }

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

var myZE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	v.acceptMeta(zn.InhMeta)
	v.acceptMeta(zn.InhMeta, evalMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteByte('\n')
	}
	ast.Walk(v, &zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as zmk.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	v.acceptMeta(m)
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (v *visitor) acceptMeta(m *meta.Meta) {
	for key, val := range m.Computed() {
		v.b.WriteStrings(key, ": ", string(val), "\n")
func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
		key := p.Key
		v.b.WriteStrings(key, ": ")
		if meta.Type(key) == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			ast.Walk(v, &is)
		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}

// WriteContent encodes the zettel content.
func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, &zn.Ast)
}
177
178
179
180
181
182
183
184

185
186
187
188
189
190
191
185
186
187
188
189
190
191

192
193
194
195
196
197
198
199







-
+







	}
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "@@@",
	ast.VerbatimComment: "%%%",
	ast.VerbatimHTML:    "@@@", // Attribute is set to {="html"}
	ast.VerbatimCode:    "```",
	ast.VerbatimProg:    "```",
	ast.VerbatimEval:    "~~~",
	ast.VerbatimMath:    "$$$",
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !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
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







-
+










-
+














-
+







			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, &cell.Inlines)
	}
}

func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
	if bn.Syntax == meta.ValueSyntaxSVG {
	if bn.Syntax == meta.SyntaxSVG {
		v.b.WriteStrings("@@@", bn.Syntax, "\n")
		v.b.Write(bn.Blob)
		v.b.WriteString("\n@@@\n")
		return
	}
	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 = set.New(
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])
			v.b.WriteBytes('\\', b)
			last = i + 1
			continue
		}
		if i < len(tn.Text)-1 {
			s := tn.Text[i : i+2]
			if escapeSeqs.Contains(s) {
			if escapeSeqs.Has(s) {
				v.b.WriteString(tn.Text[last:i])
				for j := range len(s) {
					v.b.WriteBytes('\\', s[j])
				}
				i++
				last = i + 1
				continue
406
407
408
409
410
411
412
413

414
415
416
417
418
419
420
414
415
416
417
418
419
420

421
422
423
424
425
426
427
428







-
+







		ast.Walk(v, &en.Inlines)
		v.b.WriteByte('|')
	}
	v.b.WriteStrings(en.Ref.String(), "}}")
}

func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	if en.Syntax == meta.ValueSyntaxSVG {
	if en.Syntax == meta.SyntaxSVG {
		v.b.WriteString("@@")
		v.b.Write(en.Blob)
		v.b.WriteStrings("@@{=", en.Syntax, "}")
		return
	}
	v.b.WriteString("{{TODO: display inline BLOB}}")
}
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
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







-
+
+
+
















+
+







	ast.Walk(v, &fn.Inlines)
	v.b.Write(kind)
	v.visitAttributes(fn.Attrs)
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralCode:
	case ast.LiteralZettel:
		v.writeLiteral('@', ln.Attrs, ln.Content)
	case ast.LiteralProg:
		v.writeLiteral('`', ln.Attrs, ln.Content)
	case ast.LiteralMath:
		v.b.WriteStrings("$$", string(ln.Content), "$$")
		v.visitAttributes(ln.Attrs)
	case ast.LiteralInput:
		v.writeLiteral('\'', ln.Attrs, ln.Content)
	case ast.LiteralOutput:
		v.writeLiteral('=', ln.Attrs, ln.Content)
	case ast.LiteralComment:
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
		}
		v.b.WriteString("%%")
		v.visitAttributes(ln.Attrs)
		v.b.WriteByte(' ')
		v.b.Write(ln.Content)
	case ast.LiteralHTML:
		v.writeLiteral('@', syntaxToHTML(ln.Attrs), ln.Content)
	default:
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
	}
}

func (v *visitor) writeLiteral(code byte, a attrs.Attributes, content []byte) {
	v.b.WriteBytes(code, code)
525
526
527
528
529
530
531
532

533
537
538
539
540
541
542
543

544
545







-
+

			last = i + 1
		}
	}
	v.b.WriteString(s[last:])
}

func syntaxToHTML(a attrs.Attributes) attrs.Attributes {
	return a.Clone().Set("", meta.ValueSyntaxHTML).Remove(meta.KeySyntax)
	return a.Clone().Set("", meta.SyntaxHTML).Remove(api.KeySyntax)
}

Added encoding/atom/atom.go.




























































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package atom provides an Atom encoding.
package atom

import (
	"bytes"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

// Setup initializes the Configuration.
func (c *Configuration) Setup(cfg config.Config) {
	baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)

	c.Title = cfg.GetSiteName()
	c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
		" " +
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
	c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
}

// Marshal encodes the result of a query as Atom.
func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
	atomUpdated := encoding.LastUpdated(ml, time.RFC3339)
	feedLink := c.NewURLBuilderAbs().String()

	var buf bytes.Buffer
	buf.WriteString(`<feed xmlns="http://www.w3.org/2005/Atom">` + "\n")
	xml.WriteTag(&buf, "  ", "title", c.Title)
	xml.WriteTag(&buf, "  ", "id", feedLink)
	buf.WriteString(`  <link rel="self" href="`)
	if s := q.String(); s != "" {
		strfun.XMLEscape(&buf, c.NewURLBuilderAbs().AppendQuery(s).String())
	} else {
		strfun.XMLEscape(&buf, feedLink)
	}
	buf.WriteString(`"/>` + "\n")
	if atomUpdated != "" {
		xml.WriteTag(&buf, "  ", "updated", atomUpdated)
	}
	xml.WriteTag(&buf, "  ", "generator", c.Generator)
	buf.WriteString("  <author><name>Unknown</name></author>\n")

	for _, m := range ml {
		c.marshalMeta(&buf, m)
	}

	buf.WriteString("</feed>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	entryUpdated := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
			entryUpdated = published.UTC().Format(time.RFC3339)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).String()

	buf.WriteString("  <entry>\n")
	xml.WriteTag(buf, "    ", "title", encoding.TitleAsText(m))
	xml.WriteTag(buf, "    ", "id", link)
	buf.WriteString(`    <link rel="self" href="`)
	strfun.XMLEscape(buf, link)
	buf.WriteString(`"/>` + "\n")
	buf.WriteString(`    <link rel="alternate" type="text/html" href="`)
	strfun.XMLEscape(buf, link)
	buf.WriteString(`"/>` + "\n")

	if entryUpdated != "" {
		xml.WriteTag(buf, "    ", "updated", entryUpdated)
	}
	marshalTags(buf, m)
	buf.WriteString("  </entry>\n")
}

func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
	if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
		for _, tag := range tags {
			for len(tag) > 0 && tag[0] == '#' {
				tag = tag[1:]
			}
			if tag != "" {
				buf.WriteString(`    <category term="`)
				strfun.XMLEscape(buf, tag)
				buf.WriteString("\"/>\n")
			}
		}
	}
}

Added encoding/encoding.go.














































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

// Package encoding provides helper functions for encodings.
package encoding

import (
	"time"

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

// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
	maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local)
	for _, m := range ml {
		if val, found := m.Get(api.KeyPublished); found {
			if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
				if maxPublished.Before(published) {
					maxPublished = published
				}
			}
		}
	}
	if maxPublished.Year() > 1 {
		return maxPublished.UTC().Format(timeFormat)
	}
	return ""
}

// TitleAsText returns the title of a zettel as plain text
func TitleAsText(m *meta.Meta) string { return parser.NormalizedSpacedText(m.GetTitle()) }

Added encoding/rss/rss.go.






























































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package rss provides a RSS encoding.
package rss

import (
	"bytes"
	"context"
	"time"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

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

// Setup initializes the Configuration.
func (c *Configuration) Setup(ctx context.Context, cfg config.Config) {
	baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)
	defVals := cfg.AddDefaultValues(ctx, &meta.Meta{})

	c.Title = cfg.GetSiteName()
	c.Language = defVals.GetDefault(api.KeyLang, "")
	c.Copyright = defVals.GetDefault(api.KeyCopyright, "")
	c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
		" " +
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
	c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
}

// Marshal encodes the result of a query as Atom.
func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
	rssPublished := encoding.LastUpdated(ml, time.RFC1123Z)

	atomLink := ""
	if s := q.String(); s != "" {
		atomLink = c.NewURLBuilderAbs().AppendQuery(s).String()
	}
	var buf bytes.Buffer
	buf.WriteString(`<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">` + "\n<channel>\n")
	xml.WriteTag(&buf, "  ", "title", c.Title)
	xml.WriteTag(&buf, "  ", "link", c.NewURLBuilderAbs().String())
	xml.WriteTag(&buf, "  ", "description", "")
	xml.WriteTag(&buf, "  ", "language", c.Language)
	xml.WriteTag(&buf, "  ", "copyright", c.Copyright)
	if rssPublished != "" {
		xml.WriteTag(&buf, "  ", "pubDate", rssPublished)
		xml.WriteTag(&buf, "  ", "lastBuildDate", rssPublished)
	}
	xml.WriteTag(&buf, "  ", "generator", c.Generator)
	buf.WriteString("  <docs>https://www.rssboard.org/rss-specification</docs>\n")
	if atomLink != "" {
		buf.WriteString(`  <atom:link href="`)
		strfun.XMLEscape(&buf, atomLink)
		buf.WriteString(`" rel="self" type="application/rss+xml"></atom:link>` + "\n")
	}
	for _, m := range ml {
		c.marshalMeta(&buf, m)
	}

	buf.WriteString("</channel>\n</rss>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	itemPublished := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
			itemPublished = published.UTC().Format(time.RFC1123Z)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).String()

	buf.WriteString("  <item>\n")
	xml.WriteTag(buf, "    ", "title", encoding.TitleAsText(m))
	xml.WriteTag(buf, "    ", "link", link)
	xml.WriteTag(buf, "    ", "guid", link)
	if itemPublished != "" {
		xml.WriteTag(buf, "    ", "pubDate", itemPublished)
	}
	marshalTags(buf, m)
	buf.WriteString("  </item>\n")
}

func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
	if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
		for _, tag := range tags {
			for len(tag) > 0 && tag[0] == '#' {
				tag = tag[1:]
			}
			if tag != "" {
				xml.WriteTag(buf, "    ", "category", tag)
			}
		}
	}
}

Added encoding/xml/xml.go.





































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

// Package xml provides helper for a XML-based encoding.
package xml

import (
	"bytes"

	"zettelstore.de/z/strfun"
)

// Header contains the string that should start all XML documents.
const Header = `<?xml version="1.0" encoding="UTF-8"?>` + "\n"

// WriteTag writes a simple XML tag with a given prefix and a specific value.
func WriteTag(buf *bytes.Buffer, prefix, tag, value string) {
	buf.WriteString(prefix)
	buf.WriteByte('<')
	buf.WriteString(tag)
	buf.WriteByte('>')
	strfun.XMLEscape(buf, value)
	buf.WriteString("</")
	buf.WriteString(tag)
	buf.WriteString(">\n")
}

Changes to evaluator/evaluator.go.

21
22
23
24
25
26
27
28
29
30



31
32
33
34
35
36
37
38


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

52
53
54

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


67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
21
22
23
24
25
26
27



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

53
54
55

56
57
58
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







-
-
-
+
+
+








+
+












-
+


-
+










-
-
+
+







-
+







	"fmt"
	"path"
	"strconv"
	"strings"

	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/parser/draw"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
type Port interface {
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}

// EvaluateZettel evaluates the given zettel in the given context, with the
// given ports, and the given environment.
func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) {
	switch zn.Syntax {
	case meta.ValueSyntaxNone:
	case meta.SyntaxNone:
		// AST is empty, evaluate to a description list of metadata.
		zn.Ast = evaluateMetadata(zn.Meta)
	case meta.ValueSyntaxSxn:
	case meta.SyntaxSxn:
		zn.Ast = evaluateSxn(zn.Ast)
	default:
		EvaluateBlock(ctx, port, rtConfig, &zn.Ast)
	}
}

func evaluateSxn(bs ast.BlockSlice) ast.BlockSlice {
	// Check for structure made in parser/plain/plain.go:parseSxnBlocks
	if len(bs) == 1 {
		// If len(bs) > 1 --> an error was found during parsing
		if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimCode {
			if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.ValueSyntaxSxn {
		if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimProg {
			if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.SyntaxSxn {
				rd := sxreader.MakeReader(bytes.NewReader(vn.Content))
				if objs, err := rd.ReadAll(); err == nil {
					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.VerbatimCode,
							Kind:    ast.VerbatimProg,
							Attrs:   attrs.Attributes{"": classAttr},
							Content: buf.Bytes(),
						}
					}
					return result
				}
			}
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
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







-
+








-
+









-
+

-
-
+
+


-
+







}

func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode {
	switch vn.Kind {
	case ast.VerbatimZettel:
		return e.evalVerbatimZettel(vn)
	case ast.VerbatimEval:
		if syntax, found := vn.Attrs.Get(""); found && syntax == meta.ValueSyntaxDraw {
		if syntax, found := vn.Attrs.Get(""); found && syntax == meta.SyntaxDraw {
			return draw.ParseDrawBlock(vn)
		}
	}
	return vn
}

func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode {
	m := meta.New(id.Invalid)
	m.Set(meta.KeySyntax, getSyntax(vn.Attrs, meta.ValueSyntaxText))
	m.Set(api.KeySyntax, getSyntax(vn.Attrs, meta.SyntaxText))
	zettel := zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent(vn.Content),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	return &zn.Ast
}

func getSyntax(a attrs.Attributes, defSyntax meta.Value) meta.Value {
func getSyntax(a attrs.Attributes, defSyntax string) string {
	if a != nil {
		if val, ok := a.Get(meta.KeySyntax); ok {
			return meta.Value(val)
		if val, ok := a.Get(api.KeySyntax); ok {
			return val
		}
		if val, ok := a.Get(""); ok {
			return meta.Value(val)
			return val
		}
	}
	return defSyntax
}

func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode {
	ref := tn.Ref
290
291
292
293
294
295
296
297

298
299
300
301
302
303
304
292
293
294
295
296
297
298

299
300
301
302
303
304
305
306







-
+







	ml, err := e.port.QueryMeta(e.ctx, q)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
	}
	result, _ := QueryAction(e.ctx, q, ml)
	result, _ := QueryAction(e.ctx, q, ml, e.rtConfig)
	if result != nil {
		ast.Walk(e, result)
	}
	return result
}

func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {
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
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







-
+













+
+







}

func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) }

func setMetadataFromAttributes(m *meta.Meta, a attrs.Attributes) {
	for aKey, aVal := range a {
		if meta.KeyIsValid(aKey) {
			m.Set(aKey, meta.Value(aVal))
			m.Set(aKey, aVal)
		}
	}
}

func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) {
	for i := 0; i < len(*is); i++ {
		in := (*is)[i]
		ast.Walk(e, in)
		switch n := in.(type) {
		case *ast.LinkNode:
			(*is)[i] = e.evalLinkNode(n)
		case *ast.EmbedRefNode:
			i += embedNode(is, i, e.evalEmbedRefNode(n))
		case *ast.LiteralNode:
			i += embedNode(is, i, e.evalLiteralNode(n))
		}
	}
}

func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int {
	if ln, ok := in.(*ast.InlineSlice); ok {
		*is = replaceWithInlineNodes(*is, i, *ln)
434
435
436
437
438
439
440
441

442
443
444
445
446
447
448
438
439
440
441
442
443
444

445
446
447
448
449
450
451
452







-
+







		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		e.transcludeCount++
		return createInlineErrorImage(en)
	}

	if syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)); parser.IsImageFormat(syntax) {
	if syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax); parser.IsImageFormat(syntax) {
		e.updateImageRefNode(en, zettel.Meta, syntax)
		return en
	} else if !parser.IsASTParser(syntax) {
		// Not embeddable.
		e.transcludeCount++
		return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")")
	}
498
499
500
501
502
503
504
505
















506
507

508
509
510

511
512
513
514
515
516
517
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








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

-
+


-
+







			ast.Walk(e, &is)
			if len(is) > 0 {
				en.Inlines = is
			}
		}
	}
}

func (e *evaluator) evalLiteralNode(ln *ast.LiteralNode) ast.InlineNode {
	if ln.Kind != ast.LiteralZettel {
		return ln
	}
	e.transcludeCount++
	result := e.evaluateEmbeddedInline(ln.Content, getSyntax(ln.Attrs, meta.SyntaxText))
	if len(result) == 0 {
		return &ast.LiteralNode{
			Kind:    ast.LiteralComment,
			Attrs:   map[string]string{"-": ""},
			Content: []byte("Nothing to transclude"),
		}
	}
	return &result
}

func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode {
	errorZid := id.ZidEmoji
	errorZid := id.EmojiZid
	en.Ref = ast.ParseReference(errorZid.String())
	if len(en.Inlines) == 0 {
		en.Inlines = ast.InlineSlice{&ast.TextNode{Text: "Error placeholder"}}
		en.Inlines = parser.ParseMetadata("Error placeholder")
	}
	return en
}

func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode {
	text := strings.Join(msgWords, " ")
	if ref != nil {
539
540
541
542
543
544
545
546






547
548

549
550
551
552
553
554
555
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573

574
575
576
577
578
579
580
581








+
+
+
+
+
+

-
+







		return nil
	}
	return &ast.EmbedRefNode{
		Ref:    ref,
		Syntax: ext,
	}
}

func (e *evaluator) evaluateEmbeddedInline(content []byte, syntax string) ast.InlineSlice {
	is := parser.ParseInlines(input.NewInput(content), syntax)
	ast.Walk(e, &is)
	return is
}

func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode {
	zn := parser.ParseZettel(e.ctx, zettel, string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), e.rtConfig)
	zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax), e.rtConfig)
	ast.Walk(e, &zn.Ast)
	return zn
}

func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
	if fragment == "" {
		return firstInlinesToEmbed(*bs)

Changes to evaluator/list.go.

19
20
21
22
23
24
25
26
27




28
29

30
31
32
33

34
35
36
37
38
39
40

41
42
43
44
45
46
47
48

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




64
65
66
67
68
69
70
71
72




73
74
75
76
77
78
79
19
20
21
22
23
24
25


26
27
28
29
30
31
32
33
34
35

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

52
53
54
55
56
57
58
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







-
-
+
+
+
+


+



-
+







+







-
+















+
+
+
+









+
+
+
+







	"math"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// QueryAction transforms a list of metadata according to query actions into a AST nested list.
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (ast.BlockNode, int) {
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) (ast.BlockNode, int) {
	ap := actionPara{
		ctx:    ctx,
		q:      q,
		ml:     ml,
		kind:   ast.NestedListUnordered,
		minVal: -1,
		maxVal: -1,
		title:  rtConfig.GetSiteName(),
	}
	actions := q.Actions()
	if len(actions) == 0 {
		return ap.createBlockNodeMeta("")
	}

	acts := make([]string, 0, len(actions))
	for _, act := range actions {
	for i, act := range actions {
		if strings.HasPrefix(act, api.NumberedAction[0:1]) {
			ap.kind = ast.NestedListOrdered
			continue
		}
		if strings.HasPrefix(act, api.MinAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.minVal = num
				continue
			}
		}
		if strings.HasPrefix(act, api.MaxAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.maxVal = num
				continue
			}
		}
		if act == api.TitleAction && i+1 < len(actions) {
			ap.title = strings.Join(actions[i+1:], " ")
			break
		}
		if act == api.ReIndexAction {
			continue
		}
		acts = append(acts, act)
	}
	var firstUnknowAct string
	for _, act := range acts {
		switch act {
		case api.AtomAction:
			return ap.createBlockNodeAtom(rtConfig)
		case api.RSSAction:
			return ap.createBlockNodeRSS(rtConfig)
		case api.KeysAction:
			return ap.createBlockNodeMetaKeys()
		}
		key := strings.ToLower(act)
		switch meta.Type(key) {
		case meta.TypeWord:
			return ap.createBlockNodeWord(key)
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
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







+











-
+



-
+







type actionPara struct {
	ctx    context.Context
	q      *query.Query
	ml     []*meta.Meta
	kind   ast.NestedListKind
	minVal int
	maxVal int
	title  string
}

func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		return nil, 0
	}
	items := make([]ast.ItemSlice, 0, len(ccs))
	ccs.SortByName()
	for _, cat := range ccs {
		buf.WriteString(string(cat.Name))
		buf.WriteString(cat.Name)
		items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
			Attrs:   nil,
			Ref:     ast.ParseReference(buf.String()),
			Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}},
			Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
		})})
		buf.Truncate(bufLen)
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		Items: items,
		Attrs: nil,
136
137
138
139
140
141
142
143

144
145
146
147
148
149

150
151
152
153
154
155
156
149
150
151
152
153
154
155

156
157
158
159
160
161

162
163
164
165
166
167
168
169







-
+





-
+








	para := make(ast.InlineSlice, 0, len(ccs))
	ccs.SortByName()
	for i, cat := range ccs {
		if i > 0 {
			para = append(para, &ast.TextNode{Text: " "})
		}
		buf.WriteString(string(cat.Name))
		buf.WriteString(cat.Name)
		para = append(para,
			&ast.LinkNode{
				Attrs: countMap[cat.Count],
				Ref:   ast.ParseReference(buf.String()),
				Inlines: ast.InlineSlice{
					&ast.TextNode{Text: string(cat.Name)},
					&ast.TextNode{Text: cat.Name},
				},
			},
			&ast.FormatNode{
				Kind:    ast.FormatSuper,
				Attrs:   nil,
				Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}},
			},
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
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







-
+




-
+







-
+







	ccs := arr.Counted()
	ccs.SortByName()

	var buf bytes.Buffer
	bufLen := ap.prepareSimpleQuery(&buf)
	items := make([]ast.ItemSlice, 0, len(ccs))
	for _, cat := range ccs {
		buf.WriteString(string(cat.Name))
		buf.WriteString(cat.Name)
		buf.WriteString(api.ExistOperator)
		q1 := buf.String()
		buf.Truncate(bufLen)
		buf.WriteString(api.ActionSeparator)
		buf.WriteString(string(cat.Name))
		buf.WriteString(cat.Name)
		q2 := buf.String()
		buf.Truncate(bufLen)

		items = append(items, ast.ItemSlice{ast.CreateParaNode(
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q1),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}},
				Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
			},
			&ast.TextNode{Text: " "},
			&ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "},
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q2),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}},
333
334
335
336
337
338
339


























346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
			}
		}
	}
	return result
}

func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) }

func (ap *actionPara) createBlockNodeRSS(cfg config.Config) (ast.BlockNode, int) {
	var rssConfig rss.Configuration
	rssConfig.Setup(ap.ctx, cfg)
	rssConfig.Title = ap.title
	data := rssConfig.Marshal(ap.q, ap.ml)

	return &ast.VerbatimNode{
		Kind:    ast.VerbatimProg,
		Attrs:   attrs.Attributes{"lang": "xml"},
		Content: data,
	}, len(ap.ml)
}

func (ap *actionPara) createBlockNodeAtom(cfg config.Config) (ast.BlockNode, int) {
	var atomConfig atom.Configuration
	atomConfig.Setup(cfg)
	atomConfig.Title = ap.title
	data := atomConfig.Marshal(ap.q, ap.ml)

	return &ast.VerbatimNode{
		Kind:    ast.VerbatimProg,
		Attrs:   attrs.Attributes{"lang": "xml"},
		Content: data,
	}, len(ap.ml)
}

Changes to evaluator/metadata.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
10
11
12
13
14
15
16


17
18
19
20
21
22

23
24

25
26
27
28
29

30
31
32
33
34
35
36
37

38
39
40

41
42
43
44
45

46
47
48
49
50
51
52
53







-
-
+
+




-
+

-
+




-
+







-
+


-
+




-
+







// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package evaluator

import (
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/zettel/meta"
)

func evaluateMetadata(m *meta.Meta) ast.BlockSlice {
	descrlist := &ast.DescriptionListNode{}
	for key, val := range m.All() {
	for _, p := range m.Pairs() {
		descrlist.Descriptions = append(
			descrlist.Descriptions, getMetadataDescription(key, val))
			descrlist.Descriptions, getMetadataDescription(p.Key, p.Value))
	}
	return ast.BlockSlice{descrlist}
}

func getMetadataDescription(key string, value meta.Value) ast.Description {
func getMetadataDescription(key, value string) ast.Description {
	is := convertMetavalueToInlineSlice(value, meta.Type(key))
	return ast.Description{
		Term:         ast.InlineSlice{&ast.TextNode{Text: key}},
		Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}},
	}
}

func convertMetavalueToInlineSlice(value meta.Value, dt *meta.DescriptionType) ast.InlineSlice {
func convertMetavalueToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice {
	var sliceData []string
	if dt.IsSet {
		sliceData = value.AsSlice()
		sliceData = meta.ListFromValue(value)
		if len(sliceData) == 0 {
			return nil
		}
	} else {
		sliceData = []string{string(value)}
		sliceData = []string{value}
	}
	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet

	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.TextNode{Text: " "})

Changes to go.mod.

1
2
3

4
5
6
7
8
9
10
11
12





13
14

15
16
17
18
19


20
1
2

3
4
5
6
7





8
9
10
11
12


13
14
15
16


17
18
19


-
+




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



-
-
+
+

module zettelstore.de/z

go 1.24
go 1.23

require (
	github.com/fsnotify/fsnotify v1.8.0
	github.com/yuin/goldmark v1.7.8
	golang.org/x/crypto v0.33.0
	golang.org/x/term v0.29.0
	golang.org/x/text v0.22.0
	t73f.de/r/sx v0.0.0-20250216175334-0d9b4d343d43
	t73f.de/r/sxwebs v0.0.0-20250216175901-6b946bd651a6
	golang.org/x/crypto v0.31.0
	golang.org/x/term v0.27.0
	golang.org/x/text v0.21.0
	t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5
	t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f
	t73f.de/r/zero v0.0.0-20250213090129-255dd458b5c7
	t73f.de/r/zsc v0.0.0-20250216180240-0f7af1b4d6d8
	t73f.de/r/zsc v0.0.0-20241213121052-fd15ae683e82
)

require (
	golang.org/x/sys v0.30.0 // indirect
	t73f.de/r/webs v0.0.0-20250213131522-cacd35cfa01a // indirect
	golang.org/x/sys v0.28.0 // indirect
	t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd // indirect
)

Changes to go.sum.

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














19
20
21
22


1
2
3
4














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




19
20




-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
t73f.de/r/sx v0.0.0-20250216175334-0d9b4d343d43 h1:l90+6mBm4vUeJ3j6c+NVoTa3wgC5JJB5du7qV3JwRd4=
t73f.de/r/sx v0.0.0-20250216175334-0d9b4d343d43/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg=
t73f.de/r/sxwebs v0.0.0-20250216175901-6b946bd651a6 h1:dJiHadJGPzJJelu3H+hlwpkkaLeGbteD/viFDJUR9Vk=
t73f.de/r/sxwebs v0.0.0-20250216175901-6b946bd651a6/go.mod h1:BjweG459RDdqOHBhhFWTxABN1w6xtXHgMm/PsmbXOu0=
t73f.de/r/webs v0.0.0-20250213131522-cacd35cfa01a h1:hZF5UX2+XV8DP63xT1dCQ46lfYfn/0DOS3/yuR7g1cA=
t73f.de/r/webs v0.0.0-20250213131522-cacd35cfa01a/go.mod h1:zk92hSKB4iWyT290+163seNzu350TA9XLATC9kOldqo=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 h1:ug4hohM6pK28M8Uo0o3+XvjBure2wfEtuCnHVIdqBZY=
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5/go.mod h1:VRvsWoBErPKvMieDMMk1hsh1tb9sA4ijEQWGw/TbtQ0=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f h1:VJ4S7YWy9tCJuFz5MckqUjjktPaf0kpnTkNBVRVXpo4=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f/go.mod h1:IaM+U+LvYTYeuiIS5cwZW6kcEpdwoKBYVCU7LZr4Sgk=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd h1:+7cqJonXKDso+uPvsvOPl7BiLkhj8VQT/Has8qC5VIQ=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd/go.mod h1:NSoOON8be62MfQZzlCApK27Jt2zhIa6Vrmo9RJ4tOnQ=
t73f.de/r/zero v0.0.0-20250213090129-255dd458b5c7 h1:wBenikZ+ajenn25ZXrZ/xyWlaWD4hFvOw/hGAV182Tw=
t73f.de/r/zero v0.0.0-20250213090129-255dd458b5c7/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA=
t73f.de/r/zsc v0.0.0-20250216180240-0f7af1b4d6d8 h1:GeI+faCPzet7dUf0hkTEr6gksnqFTcT7w+IbKvRpvCw=
t73f.de/r/zsc v0.0.0-20250216180240-0f7af1b4d6d8/go.mod h1:9GFjuSx7Y7tRIMF3RN3hWQx6TCOi1dLRkmKtISz3zZA=
t73f.de/r/zsc v0.0.0-20241213121052-fd15ae683e82 h1:Bl3iCW9bHx9CeIyWUMLi9xqRV7mHh/1HS6rBnt5QQQU=
t73f.de/r/zsc v0.0.0-20241213121052-fd15ae683e82/go.mod h1:sQWKzNp0I18aSFnKJjAyhL1zTWITIj1v2acWv4GvuIY=

Changes to kernel/impl/auth.go.

13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30







-



+








package impl

import (
	"errors"
	"sync"

	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

type authService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       auth.Manager
	createManager kernel.CreateAuthManagerFunc

Changes to kernel/impl/cfg.go.

17
18
19
20
21
22
23
24

25
26
27
28
29
30


31
32
33
34
35
36
37
17
18
19
20
21
22
23

24

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







-
+
-





+
+







	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

type configService struct {
	srvConfig
	mxService sync.RWMutex
	orig      *meta.Meta
	manager   box.Manager
56
57
58
59
60
61
62
63

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

64
65
66
67
68
69
70
71







-
+







	cs.logger = logger
	cs.descr = descriptionMap{
		keyDefaultCopyright: {"Default copyright", parseString, true},
		keyDefaultLicense:   {"Default license", parseString, true},
		keyDefaultVisibility: {
			"Default zettel visibility",
			func(val string) (any, error) {
				vis := meta.Value(val).AsVisibility()
				vis := meta.GetVisibility(val)
				if vis == meta.VisibilityUnknown {
					return nil, errUnknownVisibility
				}
				return vis, nil
			},
			true,
		},
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
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







-
+








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


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






-
+

-
+







				case kernel.ConfigZmkHTML:
					return config.ZettelmarkupHTML, nil
				}
				return config.NoHTML, nil
			}),
			true,
		},
		meta.KeyLang:        {"Language", parseString, true},
		api.KeyLang:         {"Language", parseString, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
		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},
		kernel.ConfigSimpleMode:      {"Simple mode", cs.noFrozen(parseBool), true},
		config.KeyListsMenuZettel:      {"Lists menu", parseZid, true},
		config.KeyShowBackLinks:        {"Show back links", parseString, true},
		config.KeyShowFolgeLinks:       {"Show folge links", parseString, true},
		config.KeyShowSequelLinks:      {"Show sequel links", parseString, true},
		config.KeyShowBackLinks:      {"Show back links", parseString, true},
		config.KeyShowFolgeLinks:     {"Show folge links", parseString, true},
		config.KeyShowSequelLinks:    {"Show sequel links", parseString, true},
		config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true},
		config.KeyShowSuccessorLinks:   {"Show successor 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.ZidDefaultHome,
		kernel.ConfigInsecureHTML:      config.NoHTML,
		meta.KeyLang:                   meta.ValueLangEN,
		keyMaxTransclusions:            int64(1024),
		keySiteName:                    "Zettelstore",
		keyYAMLHeader:                  false,
		keyZettelFileSyntax:            nil,
		kernel.ConfigSimpleMode:        false,
		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.KeyListsMenuZettel:      id.ZidTOCListsMenu,
		config.KeyShowBackLinks:        "",
		config.KeyShowFolgeLinks:       "",
		config.KeyShowSequelLinks:      "",
		config.KeyShowBackLinks:      "",
		config.KeyShowFolgeLinks:     "",
		config.KeyShowSequelLinks:    "",
		config.KeyShowSubordinateLinks: "",
		config.KeyShowSuccessorLinks:   "",
		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.ZidConfiguration)
	data := meta.New(id.ConfigurationZid)
	for _, kv := range cs.GetNextConfigList() {
		data.Set(kv.Key, meta.Value(kv.Value))
		data.Set(kv.Key, kv.Value)
	}
	cs.mxService.Lock()
	cs.orig = data
	cs.mxService.Unlock()
	return nil
}

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







-
+



-
+






-
+
+

-
+

-
+








-
+



















-
+




-
+











-
+

-
+







}

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.ZidConfiguration})
	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.ZidConfiguration)
	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 key := range cs.orig.All() {
	for _, pair := range cs.orig.Pairs() {
		key := pair.Key
		if val, ok := m.Get(key); ok {
			cs.SetConfig(key, string(val))
			cs.SetConfig(key, val)
		} else if defVal, defFound := cs.orig.Get(key); defFound {
			cs.SetConfig(key, string(defVal))
			cs.SetConfig(key, defVal)
		}
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ZidConfiguration {
	if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ConfigurationZid {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() {
			cs.mxService.RLock()
			mgr := cs.manager
			cs.mxService.RUnlock()
			if mgr != nil {
				cs.doUpdate(mgr)
			} else {
				cs.doUpdate(ci.Box)
			}
		}()
	}
}

// --- config.Config

func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
	if m != nil {
		if val, found := m.Get(key); found {
			return string(val)
			return val
		}
	}
	if user := server.GetUser(ctx); user != nil {
		if val, found := user.Get(key); found {
			return string(val)
			return val
		}
	}
	result := cs.GetCurConfig(key)
	if result == nil {
		return ""
	}
	switch val := result.(type) {
	case string:
		return val
	case bool:
		if val {
			return meta.ValueTrue
			return api.ValueTrue
		}
		return meta.ValueFalse
		return api.ValueFalse
	case id.Zid:
		return val.String()
	case int:
		return strconv.Itoa(val)
	case []string:
		return strings.Join(val, " ")
	case meta.Visibility:
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
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







-
-
+
+

-
-
+
+

-
-
+
+

-
-
+
+




-
+



-
+



















-
+

-
+
-
-
-
-
-














-
-
+
+










-
+



// AddDefaultValues enriches the given meta data with its default values.
func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta {
	if cs == nil {
		return m
	}
	result := m
	cs.mxService.RLock()
	if _, found := m.Get(meta.KeyCopyright); !found {
		result = updateMeta(result, m, meta.KeyCopyright, cs.GetCurConfig(keyDefaultCopyright).(string))
	if _, found := m.Get(api.KeyCopyright); !found {
		result = updateMeta(result, m, api.KeyCopyright, cs.GetCurConfig(keyDefaultCopyright).(string))
	}
	if _, found := m.Get(meta.KeyLang); !found {
		result = updateMeta(result, m, meta.KeyLang, cs.Get(ctx, nil, meta.KeyLang))
	if _, found := m.Get(api.KeyLang); !found {
		result = updateMeta(result, m, api.KeyLang, cs.Get(ctx, nil, api.KeyLang))
	}
	if _, found := m.Get(meta.KeyLicense); !found {
		result = updateMeta(result, m, meta.KeyLicense, cs.GetCurConfig(keyDefaultLicense).(string))
	if _, found := m.Get(api.KeyLicense); !found {
		result = updateMeta(result, m, api.KeyLicense, cs.GetCurConfig(keyDefaultLicense).(string))
	}
	if _, found := m.Get(meta.KeyVisibility); !found {
		result = updateMeta(result, m, meta.KeyVisibility, cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility).String())
	if _, found := m.Get(api.KeyVisibility); !found {
		result = updateMeta(result, m, api.KeyVisibility, cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility).String())
	}
	cs.mxService.RUnlock()
	return result
}
func updateMeta(result, m *meta.Meta, key string, val string) *meta.Meta {
func updateMeta(result, m *meta.Meta, key, val string) *meta.Meta {
	if result == m {
		result = m.Clone()
	}
	result.Set(key, meta.Value(val))
	result.Set(key, val)
	return result
}

func (cs *configService) GetHTMLInsecurity() config.HTMLInsecurity {
	return cs.GetCurConfig(kernel.ConfigInsecureHTML).(config.HTMLInsecurity)
}

// GetSiteName returns the current value of the "site-name" key.
func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) }

// GetMaxTransclusions return the maximum number of indirect transclusions.
func (cs *configService) GetMaxTransclusions() int {
	return int(cs.GetCurConfig(keyMaxTransclusions).(int64))
}

// GetYAMLHeader returns the current value of the "yaml-header" key.
func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) }

// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
func (cs *configService) GetZettelFileSyntax() []meta.Value {
func (cs *configService) GetZettelFileSyntax() []string {
	if zfs := cs.GetCurConfig(keyZettelFileSyntax); zfs != nil {
		zfsAS := zfs.([]string)
		return zfs.([]string)
		result := make([]meta.Value, len(zfsAS))
		for i, fs := range zfsAS {
			result[i] = meta.Value(fs)
		}
		return result
	}
	return nil
}

// --- config.AuthConfig

// GetSimpleMode returns true if system tuns in simple-mode.
func (cs *configService) GetSimpleMode() bool { return cs.GetCurConfig(kernel.ConfigSimpleMode).(bool) }

// GetExpertMode returns the current value of the "expert-mode" key.
func (cs *configService) GetExpertMode() bool { return cs.GetCurConfig(keyExpertMode).(bool) }

// GetVisibility returns the visibility value, or "login" if none is given.
func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility {
	if val, ok := m.Get(meta.KeyVisibility); ok {
		if vis := val.AsVisibility(); vis != meta.VisibilityUnknown {
	if val, ok := m.Get(api.KeyVisibility); ok {
		if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
			return vis
		}
	}

	vis := cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility)
	if vis != meta.VisibilityUnknown {
		return vis
	}
	cs.mxService.RLock()
	val, _ := cs.orig.Get(keyDefaultVisibility)
	vis = val.AsVisibility()
	vis = meta.GetVisibility(val)
	cs.mxService.RUnlock()
	return vis
}

Changes to kernel/impl/cmd.go.

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

26
27
28
29
30
31
32
12
13
14
15
16
17
18

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







-






+







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

package impl

import (
	"fmt"
	"io"
	"maps"
	"os"
	"runtime/metrics"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
)

type cmdSession struct {
	w        io.Writer
198
199
200
201
202
203
204

205
206

207
208
209
210
211
212
213
198
199
200
201
202
203
204
205
206

207
208
209
210
211
212
213
214







+

-
+







	},
	"start": {"start service", cmdStart},
	"stat":  {"show service statistics", cmdStat},
	"stop":  {"stop service", cmdStop},
}

func cmdHelp(sess *cmdSession, _ string, _ []string) bool {
	cmds := maps.Keys(commands)
	table := [][]string{{"Command", "Description"}}
	for _, cmd := range slices.Sorted(maps.Keys(commands)) {
	for _, cmd := range cmds {
		table = append(table, []string{cmd, commands[cmd].Text})
	}
	sess.printTable(table)
	return true
}

func cmdConfig(sess *cmdSession, cmd string, args []string) bool {
551
552
553
554
555
556
557
558

559
560
561
562
563
564
565
566
567
568
552
553
554
555
556
557
558

559


560
561
562
563
564
565
566
567







-
+
-
-








			table = append(table, []string{env[:pos], env[pos+1:]})
		}
	}
	sess.printTable(table)
	return true
}

func sortedServiceNames(sess *cmdSession) []string {
func sortedServiceNames(sess *cmdSession) []string { return maps.Keys(sess.kern.srvNames) }
	return slices.Sorted(maps.Keys(sess.kern.srvNames))
}

func getService(sess *cmdSession, name string) (serviceData, bool) {
	srvD, found := sess.kern.srvNames[name]
	if !found {
		sess.println("Unknown service", name)
	}
	return srvD, found
}

Changes to kernel/impl/config.go.

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

26
27

28
29
30
31
32
33
34
12
13
14
15
16
17
18

19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34







-





-
+


+







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

package impl

import (
	"errors"
	"fmt"
	"maps"
	"slices"
	"strconv"
	"strings"
	"sync"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

type parseFunc func(string) (any, error)
type configDescription struct {
	text    string
	parse   parseFunc
	canList bool
55
56
57
58
59
60
61
62

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

62
63
64
65
66
67
68
69







-
+







	cur      interfaceMap
	next     interfaceMap
}

func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription {
	cfg.mxConfig.RLock()
	defer cfg.mxConfig.RUnlock()
	keys := slices.Sorted(maps.Keys(cfg.descr))
	keys := maps.Keys(cfg.descr)
	result := make([]serviceConfigDescription, 0, len(keys))
	for _, k := range keys {
		text := cfg.descr[k].text
		if strings.HasSuffix(k, "-") {
			text = text + " (list)"
		}
		result = append(result, serviceConfigDescription{Key: k, Descr: text})

Changes to kernel/impl/core.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
11
12
13
14
15
16
17

18
19
20

21
22
23

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







-



-



-
+



+







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

package impl

import (
	"fmt"
	"maps"
	"net"
	"os"
	"runtime"
	"slices"
	"sync"
	"time"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type coreService struct {
	srvConfig
	started bool

	mxRecover  sync.RWMutex
103
104
105
106
107
108
109
110

111
112
113
114
115
116
117
102
103
104
105
106
107
108

109
110
111
112
113
114
115
116







-
+







func (cs *coreService) Stop(*myKernel) {
	cs.started = false
}

func (cs *coreService) GetStatistics() []kernel.KeyValue {
	cs.mxRecover.RLock()
	defer cs.mxRecover.RUnlock()
	names := slices.Sorted(maps.Keys(cs.mapRecover))
	names := maps.Keys(cs.mapRecover)
	result := make([]kernel.KeyValue, 0, 3*len(names))
	for _, n := range names {
		ri := cs.mapRecover[n]
		result = append(
			result,
			kernel.KeyValue{
				Key:   fmt.Sprintf("Recover %q / Count", n),

Changes to kernel/impl/impl.go.

26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42







-


+







	"runtime/pprof"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

// myKernel is the main internal kernel.
type myKernel struct {
	logWriter *kernelLogWriter
	logger    *logger.Logger
	wg        sync.WaitGroup

Changes to kernel/kernel.go.

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

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

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







-





+







package kernel

import (
	"io"
	"net/url"
	"time"

	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
)

// Kernel is the main internal service.
type Kernel interface {
	// Setup sets the most basic data of a software: its name, its version,
	// and when the version was created.
	Setup(progname, version string, versionTime time.Time)

Changes to logger/logger.go.

17
18
19
20
21
22
23
24

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

24
25
26
27
28
29
30
31







-
+







import (
	"context"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

// Level defines the possible log levels
type Level uint8

// Constants for Level
const (

Changes to logger/message.go.

15
16
17
18
19
20
21
22
23


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


22
23
24
25
26
27
28
29
30







-
-
+
+








import (
	"context"
	"net/http"
	"strconv"
	"sync"

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

// Message presents a message to log.
type Message struct {
	logger *Logger
	level  Level
	buf    []byte
112
113
114
115
116
117
118
119

120
121
122
123
124
125
126
112
113
114
115
116
117
118

119
120
121
122
123
124
125
126







-
+








// User adds the user-id field of the given user to the message.
func (m *Message) User(ctx context.Context) *Message {
	if m.Enabled() {
		if up := m.logger.uProvider; up != nil {
			if user := up.GetUser(ctx); user != nil {
				m.buf = append(m.buf, ", user="...)
				if userID, found := user.Get(meta.KeyUserID); found {
				if userID, found := user.Get(api.KeyUserID); found {
					m.buf = append(m.buf, userID...)
				} else {
					m.buf = append(m.buf, user.Zid.Bytes()...)
				}
			}
		}
	}

Changes to parser/blob/blob.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


11
12
13
14
15
16
17

18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
34


35
36
37
38
39
40
41
42
43

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








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







-



+




-
+





+


-
-
+
+




+


-
+
+
+
+
+
+
+
+
+
+





-
-
-
-
-
-
-
-
+













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

// Package blob provides a parser of binary data.
package blob

import (
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxGif,
		Name:          meta.SyntaxGif,
		AltNames:      nil,
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxJPEG,
		AltNames:      []string{meta.ValueSyntaxJPG},
		Name:          meta.SyntaxJPEG,
		AltNames:      []string{meta.SyntaxJPG},
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxPNG,
		Name:          meta.SyntaxPNG,
		AltNames:      nil,
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.SyntaxWebp,
		AltNames:      nil,
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
	})
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxWebp,
		AltNames:      nil,
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
	if p := parser.Get(syntax); p != nil {
		syntax = p.Name
	}
	return ast.BlockSlice{&ast.BLOBNode{
		Description: parser.ParseDescription(m),
		Syntax:      syntax,
		Blob:        []byte(inp.Src),
	}}
}

func parseInlines(*input.Input, string) ast.InlineSlice { return nil }

Changes to parser/cleaner/cleaner.go.

111
112
113
114
115
116
117


















118
119
120
121
122
123
124
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







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







	if len(*is) == 0 {
		*is = nil
		return
	}
	for _, bn := range *is {
		ast.Walk(cv, bn)
	}

	fromPos, toPos := 0, 0
	for fromPos < len(*is) {
		(*is)[toPos] = (*is)[fromPos]
		fromPos++
		switch in := (*is)[toPos].(type) {
		case *ast.LiteralNode:
			if in.Kind != ast.LiteralHTML {
				toPos++
			}
		default:
			toPos++
		}
	}
	for pos := toPos; pos < len(*is); pos++ {
		(*is)[pos] = nil // Allow excess nodes to be garbage collected.
	}
	*is = (*is)[:toPos:toPos]
}

func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
		return
	}
	if hn.Slug == "" {

Changes to parser/draw/draw.go.

17
18
19
20
21
22
23
24
25
26
27

28
29
30
31
32

33
34
35
36
37

38
39
40
41
42
43
44
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31

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







-



+




-
+





+







// metadata "syntax" of a zettel). It will be used when a zettel is evaluated.
package draw

import (
	"strconv"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxDraw,
		Name:          meta.SyntaxDraw,
		AltNames:      []string{},
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

const (
	defaultFont   = ""
	defaultScaleX = 10
	defaultScaleY = 20
55
56
57
58
59
60
61
62

63
64
65
66
67
68

69
70
71

















72
73
74
75
76
77
78
56
57
58
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







-
+





-
+



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







		scaleY = defaultScaleY
	}

	canvas, err := newCanvas(inp.Src[inp.Pos:])
	if err != nil {
		return ast.BlockSlice{ast.CreateParaNode(canvasErrMsg(err)...)}
	}
	svg := canvasToSVG(canvas, string(font), int(scaleX), int(scaleY))
	svg := canvasToSVG(canvas, font, int(scaleX), int(scaleY))
	if len(svg) == 0 {
		return ast.BlockSlice{ast.CreateParaNode(noSVGErrMsg()...)}
	}
	return ast.BlockSlice{&ast.BLOBNode{
		Description: parser.ParseDescription(m),
		Syntax:      meta.ValueSyntaxSVG,
		Syntax:      meta.SyntaxSVG,
		Blob:        svg,
	}}
}

func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
	canvas, err := newCanvas(inp.Src[inp.Pos:])
	if err != nil {
		return canvasErrMsg(err)
	}
	svg := canvasToSVG(canvas, defaultFont, defaultScaleX, defaultScaleY)
	if len(svg) == 0 {
		return noSVGErrMsg()
	}
	return ast.InlineSlice{&ast.EmbedBLOBNode{
		Attrs:   nil,
		Syntax:  meta.SyntaxSVG,
		Blob:    svg,
		Inlines: nil,
	}}
}

// ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB.
func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode {
	font := defaultFont
	if val, found := vn.Attrs.Get("font"); found {
		font = val
	}
91
92
93
94
95
96
97
98

99
100
101
102
103
104
105
109
110
111
112
113
114
115

116
117
118
119
120
121
122
123







-
+







	}
	svg := canvasToSVG(canvas, font, scaleX, scaleY)
	if len(svg) == 0 {
		return ast.CreateParaNode(noSVGErrMsg()...)
	}
	return &ast.BLOBNode{
		Description: nil, // TODO: look for attribute "summary" / "title"
		Syntax:      meta.ValueSyntaxSVG,
		Syntax:      meta.SyntaxSVG,
		Blob:        svg,
	}
}

func getScale(a attrs.Attributes, key string, defVal int) int {
	if val, found := a.Get(key); found {
		if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 {

Changes to parser/draw/draw_test.go.

12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29

30
31
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28

29
30
31







-



+






-
+


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

package draw_test

import (
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)
		parser.ParseBlocks(inp, nil, meta.ValueSyntaxDraw, config.NoHTML)
		parser.ParseBlocks(inp, nil, meta.SyntaxDraw, config.NoHTML)
	})
}

Changes to parser/markdown/markdown.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
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35
36


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







-




+




-
-
+
+




+







+
+
+
+
+







	"strings"

	gm "github.com/yuin/goldmark"
	gmAst "github.com/yuin/goldmark/ast"
	gmText "github.com/yuin/goldmark/text"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxMarkdown,
		AltNames:      []string{meta.ValueSyntaxMD},
		Name:          meta.SyntaxMarkdown,
		AltNames:      []string{meta.SyntaxMD},
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice {
	p := parseMarkdown(inp)
	return p.acceptBlockChildren(p.docNode)
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	bs := parseBlocks(inp, nil, syntax)
	return bs.FirstParagraphInlines()
}

func parseMarkdown(inp *input.Input) *mdP {
	source := []byte(inp.Src[inp.Pos:])
	parser := gm.DefaultParser()
	node := parser.Parse(gmText.NewReader(source))
	textEnc := textenc.Create()
	return &mdP{source: source, docNode: node, textEnc: textEnc}
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
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







-
+











-
+







	return &ast.HRuleNode{
		Attrs: nil, //TODO
	}
}

func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode {
	return &ast.VerbatimNode{
		Kind:    ast.VerbatimCode,
		Kind:    ast.VerbatimProg,
		Attrs:   nil, //TODO
		Content: p.acceptRawText(node),
	}
}

func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode {
	var a attrs.Attributes
	if language := node.Language(p.source); len(language) > 0 {
		a = a.Set("class", "language-"+cleanText(language, true))
	}
	return &ast.VerbatimNode{
		Kind:    ast.VerbatimCode,
		Kind:    ast.VerbatimProg,
		Attrs:   a,
		Content: p.acceptRawText(node),
	}
}

func (p *mdP) acceptRawText(node gmAst.Node) []byte {
	lines := node.Lines()
348
349
350
351
352
353
354
355

356
357
358
359
360
361
362
354
355
356
357
358
359
360

361
362
363
364
365
366
367
368







-
+







		}
		buf.Write(content[lastPos:])
		content = buf.Bytes()
	}

	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:    ast.LiteralCode,
			Kind:    ast.LiteralProg,
			Attrs:   nil, //TODO
			Content: content,
		},
	}
}

func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice {
435
436
437
438
439
440
441
442
443


444
445
446
447
441
442
443
444
445
446
447


448
449
450
451
452
453







-
-
+
+




	segs := make([][]byte, 0, node.Segments.Len())
	for i := range node.Segments.Len() {
		segment := node.Segments.At(i)
		segs = append(segs, segment.Value(p.source))
	}
	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:    ast.LiteralCode,
			Attrs:   attrs.Attributes{"": "html"},
			Kind:    ast.LiteralHTML,
			Attrs:   nil, // TODO: add HTML as language
			Content: bytes.Join(segs, nil),
		},
	}
}

Changes to parser/none/none.go.

11
12
13
14
15
16
17
18
19
20
21

22
23
24
25
26

27
28
29
30
31


32
33







11
12
13
14
15
16
17

18
19
20
21
22
23
24
25

26
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41







-



+




-
+




-
+
+


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

// Package none provides a none-parser, e.g. for zettel with just metadata.
package none

import (
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxNone,
		Name:          meta.SyntaxNone,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: false,
		ParseBlocks:   func(*input.Input, *meta.Meta, string) ast.BlockSlice { return nil },
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

func parseBlocks(*input.Input, *meta.Meta, string) ast.BlockSlice { return nil }

func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
	inp.SkipToEOL()
	return nil
}

Changes to parser/parser.go.

15
16
17
18
19
20
21
22

23
24
25
26
27

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

42
43
44
45
46
47
48
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50







-
+





+














+







package parser

import (
	"context"
	"fmt"
	"strings"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)

// Info describes a single parser.
//
// Before ParseBlocks() or ParseInlines() is called, ensure the input stream to
// be valid. This can ce achieved on calling inp.Next() after the input stream
// was created.
type Info struct {
	Name          string
	AltNames      []string
	IsASTParser   bool
	IsTextFormat  bool
	IsImageFormat bool
	ParseBlocks   func(*input.Input, *meta.Meta, string) ast.BlockSlice
	ParseInlines  func(*input.Input, string) ast.InlineSlice
}

var registry = map[string]*Info{}

// Register the parser (info) for later retrieval.
func Register(pi *Info) {
	if _, ok := registry[pi.Name]; ok {
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
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








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

-
+
-

-
+



-
+







-
-
+
+
+
+

-
-
+
+

-
+










-
+


-
+








// ParseBlocks parses some input and returns a slice of block nodes.
func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice {
	bs := Get(syntax).ParseBlocks(inp, m, syntax)
	cleaner.CleanBlockSlice(&bs, hi.AllowHTML(syntax))
	return bs
}

// ParseInlines parses some input and returns a slice of inline nodes.
func ParseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	// Do not clean, because we don't know the context where this function will be called.
	return Get(syntax).ParseInlines(inp, syntax)
}

// ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice.
// Typically used to parse the title or other metadata of type Zettelmarkup.
func ParseMetadata(value string) ast.InlineSlice {
	return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk)
}

// ParseSpacedText returns an inline slice that consists just of test and space node.
// No Zettelmarkup parsing is done. It is typically used to transform the zettel
// No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice.
// description into an inline slice.
func ParseSpacedText(s string) ast.InlineSlice {
	return ast.InlineSlice{&ast.TextNode{Text: NormalizedSpacedText(s)}}
	return ast.InlineSlice{&ast.TextNode{Text: strings.Join(meta.ListFromValue(s), " ")}}
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(strings.Fields(s), " ") }
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 summary, found := m.Get(meta.KeySummary); found {
		return ParseSpacedText(string(summary))
	if descr, found := m.Get(api.KeySummary); found {
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if title, found := m.Get(meta.KeyTitle); found {
		return ParseSpacedText(string(title))
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title/summary: " + m.Zid.String()}}
	return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title: " + m.Zid.String()}}
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {
		inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta)
	}
	if syntax == "" {
		syntax = string(inhMeta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))
		syntax = inhMeta.GetDefault(api.KeySyntax, meta.DefaultSyntax)
	}
	parseMeta := inhMeta
	if syntax == meta.ValueSyntaxNone {
	if syntax == meta.SyntaxNone {
		parseMeta = m
	}

	hi := config.NoHTML
	if rtConfig != nil {
		hi = rtConfig.GetHTMLInsecurity()
	}

Changes to parser/parser_test.go.

12
13
14
15
16
17
18
19
20
21



22
23
24
25
26
27
28
29
30
31
32

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

















55
56
57

58
59
60
61
62
63
64
65

66
67
68
12
13
14
15
16
17
18



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

32
33
34
35
36
37

















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

57
58
59
60
61
62
63
64

65
66
67
68







-
-
-
+
+
+










-
+





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


-
+







-
+



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

package parser_test

import (
	"testing"

	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

func TestParserType(t *testing.T) {
	syntaxSet := set.New(parser.GetSyntaxes()...)
	syntaxSet := strfun.NewSet(parser.GetSyntaxes()...)
	testCases := []struct {
		syntax string
		ast    bool
		image  bool
	}{
		{meta.ValueSyntaxHTML, false, false},
		{meta.ValueSyntaxCSS, false, false},
		{meta.ValueSyntaxDraw, true, false},
		{meta.ValueSyntaxGif, false, true},
		{meta.ValueSyntaxJPEG, false, true},
		{meta.ValueSyntaxJPG, false, true},
		{meta.ValueSyntaxMarkdown, true, false},
		{meta.ValueSyntaxMD, true, false},
		{meta.ValueSyntaxNone, false, false},
		{meta.ValueSyntaxPlain, false, false},
		{meta.ValueSyntaxPNG, false, true},
		{meta.ValueSyntaxSVG, false, true},
		{meta.ValueSyntaxSxn, false, false},
		{meta.ValueSyntaxText, false, false},
		{meta.ValueSyntaxTxt, false, false},
		{meta.ValueSyntaxWebp, false, true},
		{meta.ValueSyntaxZmk, true, false},
		{meta.SyntaxHTML, false, false},
		{meta.SyntaxCSS, false, false},
		{meta.SyntaxDraw, true, false},
		{meta.SyntaxGif, false, true},
		{meta.SyntaxJPEG, false, true},
		{meta.SyntaxJPG, false, true},
		{meta.SyntaxMarkdown, true, false},
		{meta.SyntaxMD, true, false},
		{meta.SyntaxNone, false, false},
		{meta.SyntaxPlain, false, false},
		{meta.SyntaxPNG, false, true},
		{meta.SyntaxSVG, false, true},
		{meta.SyntaxSxn, false, false},
		{meta.SyntaxText, false, false},
		{meta.SyntaxTxt, false, false},
		{meta.SyntaxWebp, false, true},
		{meta.SyntaxZmk, true, false},
	}
	for _, tc := range testCases {
		syntaxSet.Remove(tc.syntax)
		delete(syntaxSet, tc.syntax)
		if got := parser.IsASTParser(tc.syntax); got != tc.ast {
			t.Errorf("Syntax %q is AST: %v, but got %v", tc.syntax, tc.ast, got)
		}
		if got := parser.IsImageFormat(tc.syntax); got != tc.image {
			t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got)
		}
	}
	for syntax := range syntaxSet.Values() {
	for syntax := range syntaxSet {
		t.Errorf("Forgot to test syntax %q", syntax)
	}
}

Changes to parser/plain/plain.go.

15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31


32
33
34
35

36
37
38

39
40
41
42
43

44
45
46

47
48
49
50
51

52
53
54

55
56
57
58
59

60
61
62

63
64
65
66
67

68
69
70
71
72

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















86
87
88
89
90
91
92
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29


30
31
32
33
34
35
36
37
38

39
40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
55
56

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







-



+




-
-
+
+




+


-
+





+


-
+





+


-
+





+


-
+





+




-
+













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







package plain

import (
	"bytes"

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxTxt,
		AltNames:      []string{meta.ValueSyntaxPlain, meta.ValueSyntaxText},
		Name:          meta.SyntaxTxt,
		AltNames:      []string{meta.SyntaxPlain, meta.SyntaxText},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxHTML,
		Name:          meta.SyntaxHTML,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocksHTML,
		ParseInlines:  parseInlinesHTML,
	})
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxCSS,
		Name:          meta.SyntaxCSS,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxSVG,
		Name:          meta.SyntaxSVG,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: true,
		ParseBlocks:   parseSVGBlocks,
		ParseInlines:  parseSVGInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxSxn,
		Name:          meta.SyntaxSxn,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseSxnBlocks,
		ParseInlines:  parseSxnInlines,
	})
}

func parseBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	return doParseBlocks(inp, syntax, ast.VerbatimCode)
	return doParseBlocks(inp, syntax, ast.VerbatimProg)
}
func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	return doParseBlocks(inp, syntax, ast.VerbatimHTML)
}
func doParseBlocks(inp *input.Input, syntax string, kind ast.VerbatimKind) ast.BlockSlice {
	return ast.BlockSlice{
		&ast.VerbatimNode{
			Kind:    kind,
			Attrs:   attrs.Attributes{"": syntax},
			Content: inp.ScanLineContent(),
		},
	}
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	return doParseInlines(inp, syntax, ast.LiteralProg)
}
func parseInlinesHTML(inp *input.Input, syntax string) ast.InlineSlice {
	return doParseInlines(inp, syntax, ast.LiteralHTML)
}
func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{&ast.LiteralNode{
		Kind:    kind,
		Attrs:   attrs.Attributes{"": syntax},
		Content: append([]byte(nil), inp.Src[0:inp.Pos]...),
	}}
}

func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	is := parseSVGInlines(inp, syntax)
	if len(is) == 0 {
		return nil
	}
	return ast.BlockSlice{ast.CreateParaNode(is...)}
118
119
120
121
122
123
124
125

126
127
128
129
130
131
132
133
134
135
136









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







-
+











+
+
+
+
+
+
+
+
+
}

func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
	_, err := rd.ReadAll()
	result := ast.BlockSlice{
		&ast.VerbatimNode{
			Kind:    ast.VerbatimCode,
			Kind:    ast.VerbatimProg,
			Attrs:   attrs.Attributes{"": syntax},
			Content: inp.ScanLineContent(),
		},
	}
	if err != nil {
		result = append(result, ast.CreateParaNode(&ast.TextNode{
			Text: err.Error(),
		}))
	}
	return result
}

func parseSxnInlines(inp *input.Input, syntax string) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{&ast.LiteralNode{
		Kind:    ast.LiteralProg,
		Attrs:   attrs.Attributes{"": syntax},
		Content: append([]byte(nil), inp.Src[0:inp.Pos]...),
	}}
}

Changes to parser/plain/plain_test.go.

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



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






38
39
40
41
42

43
44

45
46
47
48
49
50
12
13
14
15
16
17
18

19



20
21
22
23
24
25
26
27
28
29
30






31
32
33
34
35
36
37
38
39
40

41
42

43
44
45
46
47
48
49







-

-
-
-
+
+
+








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




-
+

-
+






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

package plain_test

import (
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func TestParseSVG(t *testing.T) {
	testCases := []struct {
		name string
		src  string
		exp  string
	}{
		{"common", " <svg bla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg bla\")))"},
		{"inkscape", "<svg\nbla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg\\nbla\")))"},
		{"selfmade", "<svg>", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg>\")))"},
		{"error", "<svgbla", "(BLOCK)"},
		{"error-", "<svg-bla", "(BLOCK)"},
		{"error#", "<svg2bla", "(BLOCK)"},
		{"common", " <svg bla", "(INLINE (EMBED-BLOB () \"svg\" \"<svg bla\"))"},
		{"inkscape", "<svg\nbla", "(INLINE (EMBED-BLOB () \"svg\" \"<svg\\nbla\"))"},
		{"selfmade", "<svg>", "(INLINE (EMBED-BLOB () \"svg\" \"<svg>\"))"},
		{"error", "<svgbla", "(INLINE)"},
		{"error-", "<svg-bla", "(INLINE)"},
		{"error#", "<svg2bla", "(INLINE)"},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			inp := input.NewInput([]byte(tc.src))
			bs := parser.ParseBlocks(inp, nil, meta.ValueSyntaxSVG, config.NoHTML)
			is := parser.ParseInlines(inp, meta.SyntaxSVG)
			trans := szenc.NewTransformer()
			lst := trans.GetSz(&bs)
			lst := trans.GetSz(&is)
			if got := lst.String(); tc.exp != got {
				t.Errorf("\nexp: %q\ngot: %q", tc.exp, got)
			}
		})
	}
}

Changes to parser/zettelmark/block.go.

198
199
200
201
202
203
204
205

206
207
208
209
210
211
212
198
199
200
201
202
203
204

205
206
207
208
209
210
211
212







-
+







		return nil, false
	}
	var kind ast.VerbatimKind
	switch fch {
	case '@':
		kind = ast.VerbatimZettel
	case '`', runeModGrave:
		kind = ast.VerbatimCode
		kind = ast.VerbatimProg
	case '%':
		kind = ast.VerbatimComment
	case '~':
		kind = ast.VerbatimEval
	case '$':
		kind = ast.VerbatimMath
	default:

Changes to parser/zettelmark/inline.go.

14
15
16
17
18
19
20

21
22

23













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







+


+

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







package zettelmark

import (
	"bytes"
	"fmt"
	"strings"

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

// parseInlineSlice parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) {
	inp := cp.inp
	for inp.Ch != input.EOS {
		in := cp.parseInline()
		if in == nil {
			break
		}
		ins = append(ins, in)
	}
	return ins
}

func (cp *zmkP) parseInline() ast.InlineNode {
	inp := cp.inp
	pos := inp.Pos
	if cp.nestingLevel <= maxNestingLevel {
		cp.nestingLevel++
		defer func() { cp.nestingLevel-- }()
53
54
55
56
57
58
59
60

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

75
76
77
78
79
80
81
82







-
+







			if inp.Ch == '{' {
				in, success = cp.parseEmbed()
			}
		case '%':
			in, success = cp.parseComment()
		case '_', '*', '>', '~', '^', ',', '"', '#', ':':
			in, success = cp.parseFormat()
		case '\'', '`', '=', runeModGrave:
		case '@', '\'', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '$':
			in, success = cp.parseLiteralMath()
		case '\\':
			return cp.parseBackslash()
		case '-':
			in, success = cp.parseNdash()
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







-
+







		return cp.parseBackslashRest()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '`', runeModGrave, '$', '=', '\\', '-', '&':
		case input.EOS, '\n', '\r', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp
426
427
428
429
430
431
432

433
434


435
436
437
438
439
440
441
441
442
443
444
445
446
447
448


449
450
451
452
453
454
455
456
457







+
-
-
+
+







			}
			fn.Inlines = append(fn.Inlines, in)
		}
	}
}

var mapRuneLiteral = map[rune]ast.LiteralKind{
	'@':          ast.LiteralZettel,
	'`':          ast.LiteralCode,
	runeModGrave: ast.LiteralCode,
	'`':          ast.LiteralProg,
	runeModGrave: ast.LiteralProg,
	'\'':         ast.LiteralInput,
	'=':          ast.LiteralOutput,
	// No '$': ast.LiteralMath, because paring literal math is a little different
}

func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) {
	inp := cp.inp
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
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







-
-
-
+
-
-









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







		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Peek() == fch {
				inp.Next()
				inp.Next()
				return &ast.LiteralNode{
					Kind:    kind,
					Attrs:   cp.parseInlineAttributes(),
				return createLiteralNode(kind, cp.parseInlineAttributes(), buf.Bytes()), true
					Content: buf.Bytes(),
				}, true
			}
			buf.WriteRune(fch)
			inp.Next()
		} else {
			tn := cp.parseText()
			buf.WriteString(tn.Text)
		}
	}
}

func createLiteralNode(kind ast.LiteralKind, a attrs.Attributes, content []byte) *ast.LiteralNode {
	if kind == ast.LiteralZettel {
		if val, found := a.Get(""); found && val == meta.SyntaxHTML {
			kind = ast.LiteralHTML
			a = a.Remove("")
		}
	}
	return &ast.LiteralNode{
		Kind:    kind,
		Attrs:   a,
		Content: content,
	}
}

func (cp *zmkP) parseLiteralMath() (res ast.InlineNode, success bool) {
	inp := cp.inp
	inp.Next() // read 2nd formatting character
	if inp.Ch != '$' {
		return nil, false
	}

Changes to parser/zettelmark/post-processor.go.

20
21
22
23
24
25
26






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







+
+
+
+
+
+







)

// postProcessBlocks is the entry point for post-processing a list of block nodes.
func postProcessBlocks(bs *ast.BlockSlice) {
	pp := postProcessor{}
	ast.Walk(&pp, bs)
}

// postProcessInlines is the entry point for post-processing a list of inline nodes.
func postProcessInlines(is *ast.InlineSlice) {
	pp := postProcessor{}
	ast.Walk(&pp, is)
}

// postProcessor is a visitor that cleans the abstract syntax tree.
type postProcessor struct {
	inVerse bool
}

func (pp *postProcessor) Visit(node ast.Node) ast.Visitor {

Changes to parser/zettelmark/zettelmark.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
11
12
13
14
15
16
17

18
19
20
21

22

23


24
25
26
27
28
29

30
31
32
33
34
35
36
37








38
39
40
41
42
43
44
45
46







47
48
49


50
51




52
53
54
55
56
57
58







-




-

-

-
-
+
+




-
+





+

-
-
-
-
-
-
-
-









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







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

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"log"
	"strings"
	"unicode"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/sz/zmk"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/ast/sztrans"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.ValueSyntaxZmk,
		Name:          meta.SyntaxZmk,
		AltNames:      nil,
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
	// parser.Register(&parser.Info{
	// 	Name:          meta.ValueSyntaxZmk,
	// 	AltNames:      nil,
	// 	IsASTParser:   true,
	// 	IsTextFormat:  true,
	// 	IsImageFormat: false,
	// 	ParseBlocks:   parseZmkBlocks,
	// })
}

func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice {
	parser := &zmkP{inp: inp}
	bs := parser.parseBlockSlice()
	postProcessBlocks(&bs)
	return bs
}

// ParseZmkBlocks parses input with the external parser.
//
// Note: this function is currently exported to keep some linter silent.
// In the future, this function will become private.
func ParseZmkBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice {
	if obj := zmk.ParseBlocks(inp); obj != nil {
		bs, err := sztrans.GetBlockSlice(obj)
func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
	parser := &zmkP{inp: inp}
	is := parser.parseInlineSlice()
		if err == nil {
			return bs
	postProcessInlines(&is)
	return is
		}
		log.Printf("sztrans error: %v, for %v\n", err, obj)
	}
	return nil
}

type zmkP struct {
	inp          *input.Input             // Input stream
	lists        []*ast.NestedListNode    // Stack of lists
	table        *ast.TableNode           // Current table
	descrl       *ast.DescriptionListNode // Current description list

Changes to parser/zettelmark/zettelmark_fuzz_test.go.

12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29

30
31
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28

29
30
31







-



+






-
+


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

package zettelmark_test

import (
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)
		parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)
		parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML)
	})
}

Changes to parser/zettelmark/zettelmark_test.go.

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

28
29
30
31
32
33
34
16
17
18
19
20
21
22

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







-




+








import (
	"fmt"
	"strings"
	"testing"

	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

type TestCase struct{ source, want string }
type TestCases []TestCase

func replace(s string, tcs TestCases) TestCases {
	var testCases TestCases
44
45
46
47
48
49
50
51

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

51
52
53
54
55
56
57
58







-
+







func checkTcs(t *testing.T, tcs TestCases) {
	t.Helper()

	for tcn, tc := range tcs {
		t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) {
			st.Helper()
			inp := input.NewInput([]byte(tc.source))
			bns := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)
			bns := parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML)
			var tv TestVisitor
			ast.Walk(&tv, &bns)
			got := tv.String()
			if tc.want != got {
				st.Errorf("\nwant=%q\n got=%q", tc.want, got)
			}
		})
326
327
328
329
330
331
332
333

334
335
336
337
338
339
340
326
327
328
329
330
331
332

333
334
335
336
337
338
339
340







-
+







		{"__**a**__", "(PARA {_ {* a}})"},
		{"__**__**", "(PARA __ {* __})"},
	})
}

func TestLiteral(t *testing.T) {
	t.Parallel()
	for _, ch := range []string{"`", "'", "="} {
	for _, ch := range []string{"@", "`", "'", "="} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
950
951
952
953
954
955
956
957

958
959
960
961
962
963
964
950
951
952
953
954
955
956

957
958
959
960
961
962
963
964







-
+







		return tv
	}
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "(ZETTEL",
	ast.VerbatimCode:    "(PROG",
	ast.VerbatimProg:    "(PROG",
	ast.VerbatimEval:    "(EVAL",
	ast.VerbatimMath:    "(MATH",
	ast.VerbatimComment: "(COMMENT",
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  "(SPAN",
988
989
990
991
992
993
994
995


996
997
998
999
1000
1001
1002
988
989
990
991
992
993
994

995
996
997
998
999
1000
1001
1002
1003







-
+
+







	ast.FormatSub:    ',',
	ast.FormatQuote:  '"',
	ast.FormatMark:   '#',
	ast.FormatSpan:   ':',
}

var mapLiteralKind = map[ast.LiteralKind]rune{
	ast.LiteralCode:    '`',
	ast.LiteralZettel:  '@',
	ast.LiteralProg:    '`',
	ast.LiteralInput:   '\'',
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
	ast.LiteralMath:    '$',
}

func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) {

Changes to query/compiled.go.

13
14
15
16
17
18
19
20
21


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


20
21
22
23
24
25
26
27
28







-
-
+
+








package query

import (
	"math/rand/v2"
	"slices"

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

// Compiled is a compiled query, to be used in a Box
type Compiled struct {
	hasQuery bool
	seed     int
	pick     int

Changes to query/context.go.

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

25
26

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
12
13
14
15
16
17
18

19

20
21

22


23
24
25
26
27
28
29
30

31
32
33
34
35
36
37







-

-


-
+
-
-
+







-







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

package query

import (
	"container/heap"
	"context"
	"iter"
	"math"
	"slices"

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

// ContextSpec contains all specification values for calculating a context.
type ContextSpec struct {
	Direction ContextDirection
	MaxCost   int
	MaxCount  int
	MinCount  int
	Full      bool
}

// ContextDirection specifies the direction a context should be calculated.
type ContextDirection uint8

// Constants for ContextDirection.
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

86
87
88

89
90

91
92
93
94
95
96
97


98
99
100
101

102


103
104
105
106
107
108
109


110
111
112
113
114
115


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


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

159
160
161

162
163
164

165
166
167
168

169
170
171

172
173
174

175
176

177
178
179
180
181
182
183
184
185
186
187
188
189
190


191
192
193
194

195
196

197
198
199
200

201
202

203
204
205
206
207
208
209
210
211

212
213

214
215
216
217
218
219

220
221

222
223

224
225
226
227
228
229
230


231
232












233
234
235
236
237
238
239
240
241
242
243
244


245
246
247

248
249
250
251
252
253

254
255
256

257
258

259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276

277
278
279
280

281
282
283
284

285
286
287
288
289
290

291
292
293
294
295
296
297
298
299
300
301
302
303
304

305
306
307




308
309
310
311
312
313
314
315
316
317
318
319
320

321
322

323
324
325

326
327
328
329
330

331
332
333


334
335
336
337
338
339
61
62
63
64
65
66
67

68
69
70
71
72
73
74
75
76
77
78
79

80
81
82

83
84

85
86
87
88
89
90


91
92
93
94
95
96
97

98
99
100
101
102
103
104


105
106

107
108
109


110
111































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

125
126
127

128

129

130
131
132
133

134
135
136

137
138


139
140

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


154
155
156
157
158

159
160

161
162
163
164

165
166

167
168
169
170
171
172
173
174
175

176
177

178
179
180
181
182
183

184
185

186
187

188
189
190
191
192
193


194
195
196

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212








213
214
215
216

217
218
219
220
221
222

223
224
225

226


227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244

245
246
247
248

249
250
251
252

253
254
255
256
257
258

259
260
261
262
263
264
265
266
267
268
269
270
271
272

273
274
275

276
277
278
279
280
281
282
283
284
285
286




287

288
289

290
291
292

293





294



295
296













-












-
+


-
+

-
+





-
-
+
+




+
-
+
+





-
-
+
+
-



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











-
+


-
+
-

-
+



-
+


-
+

-
-
+

-
+












-
-
+
+



-
+

-
+



-
+

-
+








-
+

-
+





-
+

-
+

-
+





-
-
+
+

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




-
-
-
-
-
-
-
-
+
+


-
+





-
+


-
+
-
-
+

















-
+



-
+



-
+





-
+













-
+


-
+
+
+
+







-
-
-
-

-
+

-
+


-
+
-
-
-
-
-
+
-
-
-
+
+
-
-
-
-
-
-
		pe.writeString(api.BackwardDirective)
	case ContextDirForward:
		pe.printSpace()
		pe.writeString(api.ForwardDirective)
	}
	pe.printPosInt(api.CostDirective, spec.MaxCost)
	pe.printPosInt(api.MaxDirective, spec.MaxCount)
	pe.printPosInt(api.MinDirective, spec.MinCount)
}

// Execute the specification.
func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta {
	maxCost := float64(spec.MaxCost)
	if maxCost <= 0 {
		maxCost = 17
	}
	maxCount := spec.MaxCount
	if maxCount <= 0 {
		maxCount = 200
	}
	tasks := newQueue(startSeq, maxCost, maxCount, spec.MinCount, port)
	tasks := newQueue(startSeq, maxCost, maxCount, port)
	isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward
	isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward
	result := make([]*meta.Meta, 0, max(spec.MinCount, 16))
	result := []*meta.Meta{}
	for {
		m, cost, level := tasks.next()
		m, cost := tasks.next()
		if m == nil {
			break
		}
		result = append(result, m)

		for key, val := range m.ComputedRest() {
			tasks.addPair(ctx, key, val, cost, level, isBackward, isForward)
		for _, p := range m.ComputedPairsRest() {
			tasks.addPair(ctx, p.Key, p.Value, cost, isBackward, isForward)
		}
		if !spec.Full {
			continue
		}
		if tags, found := m.GetList(api.KeyTags); found {
		tasks.addTags(ctx, m.GetFields(meta.KeyTags), cost, level)
			tasks.addTags(ctx, tags, cost)
		}
	}
	return result
}

type ztlCtxItem struct {
	cost  float64
	meta  *meta.Meta
	cost float64
	meta *meta.Meta
	level uint
}
type ztlCtxQueue []ztlCtxItem

func (q ztlCtxQueue) Len() int { return len(q) }
func (q ztlCtxQueue) Less(i, j int) bool {
func (q ztlCtxQueue) Len() int           { return len(q) }
func (q ztlCtxQueue) Less(i, j int) bool { return q[i].cost < q[j].cost }
	levelI, levelJ := q[i].level, q[j].level
	if levelI == 0 {
		if levelJ == 0 {
			return q[i].meta.Zid < q[j].meta.Zid
		}
		return true
	}
	if levelI == 1 {
		if levelJ == 0 {
			return false
		}
		if levelJ == 1 {
			costI, costJ := q[i].cost, q[j].cost
			if costI == costJ {
				return q[i].meta.Zid < q[j].meta.Zid
			}
			return costI < costJ
		}
		return true
	}
	if levelJ == 0 || levelJ == 1 {
		return false
	}
	costI, costJ := q[i].cost, q[j].cost
	if costI == costJ {
		return q[i].meta.Zid < q[j].meta.Zid
	}
	return costI < costJ
}
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) 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     *idset.Set
	seen     *id.Set
	queue    ztlCtxQueue
	maxCost  float64
	maxCount int
	limit    int
	minCount int
	tagMetas map[string][]*meta.Meta
	tagZids  map[string]*idset.Set // just the zids of tagMetas
	tagZids  map[string]*id.Set    // just the zids of tagMetas
	metaZid  map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags
}

func newQueue(startSeq []*meta.Meta, maxCost float64, maxCount, minCount int, port ContextPort) *contextTask {
func newQueue(startSeq []*meta.Meta, maxCost float64, limit int, port ContextPort) *contextTask {
	result := &contextTask{
		port:     port,
		seen:     idset.New(),
		seen:     id.NewSet(),
		maxCost:  maxCost,
		maxCount: max(maxCount, minCount),
		minCount: minCount,
		limit:    limit,
		tagMetas: make(map[string][]*meta.Meta),
		tagZids:  make(map[string]*idset.Set),
		tagZids:  make(map[string]*id.Set),
		metaZid:  make(map[id.Zid]*meta.Meta),
	}

	queue := make(ztlCtxQueue, 0, len(startSeq))
	for _, m := range startSeq {
		queue = append(queue, ztlCtxItem{cost: 1, meta: m})
	}
	heap.Init(&queue)
	result.queue = queue
	return result
}

func (ct *contextTask) addPair(ctx context.Context, key string, value meta.Value, curCost float64, level uint, isBackward, isForward bool) {
	if key == meta.KeyBack {
func (ct *contextTask) addPair(ctx context.Context, key, value string, curCost float64, isBackward, isForward bool) {
	if key == api.KeyBack {
		return
	}
	newCost := curCost + contextCost(key)
	if key == meta.KeyBackward {
	if key == api.KeyBackward {
		if isBackward {
			ct.addIDSet(ctx, newCost, level, value)
			ct.addIDSet(ctx, newCost, value)
		}
		return
	}
	if key == meta.KeyForward {
	if key == api.KeyForward {
		if isForward {
			ct.addIDSet(ctx, newCost, level, value)
			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, level, value)
		ct.addID(ctx, newCost, value)
	} else if t == meta.TypeIDSet {
		ct.addIDSet(ctx, newCost, level, value)
		ct.addIDSet(ctx, newCost, value)
	}
}

func contextCost(key string) float64 {
	switch key {
	case meta.KeyFolge, meta.KeyPrecursor:
	case api.KeyFolge, api.KeyPrecursor:
		return 0.1
	case meta.KeySequel, meta.KeyPrequel:
	case api.KeySequel, api.KeyPrequel:
		return 1.0
	case meta.KeySuccessors, meta.KeyPredecessor:
	case api.KeySuccessors, api.KeyPredecessor:
		return 7
	}
	return 2
}

func (ct *contextTask) addID(ctx context.Context, newCost float64, level uint, value meta.Value) {
	if zid, errParse := id.Parse(string(value)); errParse == nil {
func (ct *contextTask) addID(ctx context.Context, newCost float64, value string) {
	if zid, errParse := id.Parse(value); errParse == nil {
		if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil {
			ct.addMeta(m, newCost, level)
			ct.addMeta(m, newCost)
		}
	}
}

func (ct *contextTask) addMeta(m *meta.Meta, newCost float64) {
	// If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all
	// other zettel that are directly reachable, without taking the cost into account.
	// Of course, the limit ist still relevant.
	if !ct.hasLimit() && (ct.seen.Length() <= 1 || ct.maxCost == 0 || newCost <= ct.maxCost) {
		if !ct.seen.Contains(m.Zid) {
			heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m})
		}
	}
}

func (ct *contextTask) addMeta(m *meta.Meta, newCost float64, level uint) {
	if !ct.seen.Contains(m.Zid) {
		heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m, level: level + 1})
	}
}

func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, level uint, value meta.Value) {
	elems := value.AsSlice()
func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, value string) {
	elems := meta.ListFromValue(value)
	refCost := referenceCost(newCost, len(elems))
	for _, val := range elems {
		ct.addID(ctx, refCost, level, meta.Value(val))
		ct.addID(ctx, refCost, val)
	}
}

func referenceCost(baseCost float64, numReferences int) float64 {
	nRefs := float64(numReferences)
	return nRefs*math.Log2(nRefs+1) + baseCost - 1
	return nRefs*math.Log2(nRefs+1) + baseCost
}

func (ct *contextTask) addTags(ctx context.Context, tagiter iter.Seq[string], baseCost float64, level uint) {
func (ct *contextTask) addTags(ctx context.Context, tags []string, baseCost float64) {
	tags := slices.Collect(tagiter)
	var zidSet *idset.Set
	var zidSet *id.Set
	for _, tag := range tags {
		zs := ct.updateTagData(ctx, tag)
		zidSet = zidSet.IUnion(zs)
	}
	zidSet.ForEach(func(zid id.Zid) {
		minCost := math.MaxFloat64
		costFactor := 1.1
		for _, tag := range tags {
			tagZids := ct.tagZids[tag]
			if tagZids.Contains(zid) {
				cost := tagCost(baseCost, tagZids.Length())
				if cost < minCost {
					minCost = cost
				}
				costFactor /= 1.1
			}
		}
		ct.addMeta(ct.metaZid[zid], minCost*costFactor, level)
		ct.addMeta(ct.metaZid[zid], minCost*costFactor)
	})
}

func (ct *contextTask) updateTagData(ctx context.Context, tag string) *idset.Set {
func (ct *contextTask) updateTagData(ctx context.Context, tag string) *id.Set {
	if _, found := ct.tagMetas[tag]; found {
		return ct.tagZids[tag]
	}
	q := Parse(meta.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + meta.KeyID)
	q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
	ml, err := ct.port.SelectMeta(ctx, nil, q)
	if err != nil {
		ml = nil
	}
	ct.tagMetas[tag] = ml
	zids := idset.NewCap(len(ml))
	zids := id.NewSetCap(len(ml))
	for _, m := range ml {
		zid := m.Zid
		zids = zids.Add(zid)
		if _, found := ct.metaZid[zid]; !found {
			ct.metaZid[zid] = m
		}
	}
	ct.tagZids[tag] = zids
	return zids
}

func tagCost(baseCost float64, numTags int) float64 {
	nTags := float64(numTags)
	return nTags*math.Log2(nTags+1) + baseCost - 1
	return nTags*math.Log2(nTags+1) + baseCost
}

func (ct *contextTask) next() (*meta.Meta, float64, uint) {
func (ct *contextTask) next() (*meta.Meta, float64) {
	if ct.hasLimit() {
		return nil, -1
	}
	for len(ct.queue) > 0 {
		item := heap.Pop(&ct.queue).(ztlCtxItem)
		m := item.meta
		zid := m.Zid
		if ct.seen.Contains(zid) {
			continue
		}
		cost, level := item.cost, item.level
		if ct.hasEnough(cost, level) {
			break
		}
		ct.seen.Add(zid)
		return m, cost, item.level
		return m, item.cost
	}
	return nil, -1, 0
	return nil, -1
}

func (ct *contextTask) hasEnough(cost float64, level uint) bool {
func (ct *contextTask) hasLimit() bool {
	if level <= 1 {
		// Always add direct descendants of the initial zettel
		return false
	}
	length := ct.seen.Length()
	limit := ct.limit
	if minCount := ct.minCount; 0 < minCount && minCount > length {
		return false
	}
	return limit > 0 && ct.seen.Length() >= limit
}
	if maxCount := ct.maxCount; 0 < maxCount && maxCount <= length {
		return true
	}
	maxCost := ct.maxCost
	return maxCost == 0.0 || maxCost <= cost
}

Changes to query/parser.go.

13
14
15
16
17
18
19
20

21
22
23


24
25
26
27
28
29
30
13
14
15
16
17
18
19

20



21
22
23
24
25
26
27
28
29







-
+
-
-
-
+
+








package query

import (
	"strconv"

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

// Parse the query specification and return a Query object.
func Parse(spec string) (q *Query) { return q.Parse(spec) }

// Parse the query string and update the Query object.
func (q *Query) Parse(spec string) *Query {
77
78
79
80
81
82
83
84

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

83
84
85
86
87
88
89
90







-
+







func (ps *parserState) parse(q *Query) *Query {
	inp := ps.inp
	inp.SkipSpace()
	if ps.mustStop() {
		return q
	}
	firstPos := inp.Pos
	zidSet := idset.New()
	zidSet := id.NewSet()
	for {
		pos := inp.Pos
		zid, found := ps.scanZid()
		if !found {
			inp.SetPos(pos)
			break
		}
229
230
231
232
233
234
235
236

237
238
239
240
241
242
243
244
245
246
247
248
249
228
229
230
231
232
233
234

235






236
237
238
239
240
241
242







-
+
-
-
-
-
-
-







		if ps.acceptKwArgs(api.CostDirective) {
			if ps.parseCost(spec) {
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MaxDirective) {
			if ps.parseMaxCount(spec) {
			if ps.parseCount(spec) {
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MinDirective) {
			if ps.parseMinCount(spec) {
				continue
			}
		}

		inp.SetPos(pos)
		break
	}
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
250
251
252
253
254
255
256

257
258
259
260
261
262
263
264










265
266
267
268
269
270
271







-
+







-
-
-
-
-
-
-
-
-
-







		return false
	}
	if spec.MaxCost == 0 || spec.MaxCost >= num {
		spec.MaxCost = num
	}
	return true
}
func (ps *parserState) parseMaxCount(spec *ContextSpec) bool {
func (ps *parserState) parseCount(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
	}
	if spec.MaxCount == 0 || spec.MaxCount >= num {
		spec.MaxCount = num
	}
	return true
}
func (ps *parserState) parseMinCount(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
	}
	if spec.MinCount == 0 || spec.MinCount <= num {
		spec.MinCount = num
	}
	return true
}

func (ps *parserState) parseUnlinked(q *Query) *Query {
	inp := ps.inp

	spec := &UnlinkedSpec{}
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
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







-
+



-
+


-
+




-
+







	} else if len(text) == 0 {
		// Only an empty search operation is found -> ignore it
		return q
	}
	q = createIfNeeded(q)
	if hasOp {
		if key == nil {
			q.addSearch(expValue{meta.Value(string(text)), op})
			q.addSearch(expValue{string(text), op})
		} else {
			last := len(q.terms) - 1
			if q.terms[last].mvals == nil {
				q.terms[last].mvals = expMetaValues{string(key): {expValue{meta.Value(string(text)), op}}}
				q.terms[last].mvals = expMetaValues{string(key): {expValue{string(text), op}}}
			} else {
				sKey := string(key)
				q.terms[last].mvals[sKey] = append(q.terms[last].mvals[sKey], expValue{meta.Value(string(text)), op})
				q.terms[last].mvals[sKey] = append(q.terms[last].mvals[sKey], expValue{string(text), op})
			}
		}
	} else {
		// Assert key == nil
		q.addSearch(expValue{meta.Value(string(text)), cmpMatch})
		q.addSearch(expValue{string(text), cmpMatch})
	}
	return q
}

func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) {
	inp := ps.inp
	pos := inp.Pos

Changes to query/parser_test.go.

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

39
40
41
42
43
44
45
46

47
48
49
50
51
52
53







-








-







		{"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"},
		{"1 CONTEXT CONTEXT", "00000000000001 CONTEXT CONTEXT"},
		{"00000000000001 CONTEXT", "00000000000001 CONTEXT"},
		{"100000000000001 CONTEXT", "100000000000001 CONTEXT"},
		{"1 CONTEXT FULL", "00000000000001 CONTEXT FULL"},
		{"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 MIN 7", "00000000000001 CONTEXT MIN 7"}, {"1 CONTEXT MIN y", "00000000000001 CONTEXT MIN 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"},

Changes to query/print.go.

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


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


18
19
20
21

22
23
24
25
26
27
28
29
30







-
-




-
+
+







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

package query

import (
	"io"
	"maps"
	"slices"
	"strconv"
	"strings"

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

var op2string = map[compareOp]string{
	cmpExist:     api.ExistOperator,
	cmpNotExist:  api.ExistNotOperator,
	cmpEqual:     api.SearchOperatorEqual,
	cmpNotEqual:  api.SearchOperatorNotEqual,
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
74
75

76
77
78
79
80
81
82
58
59
60
61
62
63
64

65
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81







-
+








-
+







	for _, d := range q.directives {
		d.Print(&env)
	}
	for i, term := range q.terms {
		if i > 0 {
			env.writeString(" OR")
		}
		for _, name := range slices.Sorted(maps.Keys(term.keys)) {
		for _, name := range maps.Keys(term.keys) {
			env.printSpace()
			env.writeString(name)
			if op := term.keys[name]; op == cmpExist || op == cmpNotExist {
				env.writeString(op2string[op])
			} else {
				env.writeStrings(api.ExistOperator, " ", name, api.ExistNotOperator)
			}
		}
		for _, name := range slices.Sorted(maps.Keys(term.mvals)) {
		for _, name := range maps.Keys(term.mvals) {
			env.printExprValues(name, term.mvals[name])
		}
		if len(term.search) > 0 {
			env.printExprValues("", term.search)
		}
	}
	env.printPosInt(api.PickDirective, q.pick)
140
141
142
143
144
145
146
147

148
149
150
151
152
153
154
139
140
141
142
143
144
145

146
147
148
149
150
151
152
153







-
+







		default:
			if s, found := op2string[op]; found {
				pe.writeString(s)
			} else {
				pe.writeString("%" + strconv.Itoa(int(op)))
			}
		}
		if s := string(val.value); s != "" {
		if s := val.value; s != "" {
			pe.writeString(s)
		}
	}
}

// Human returns the query as a human readable string.
func (q *Query) Human() string {
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
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







-
+














-
+







		d.Print(&env)
	}
	for i, term := range q.terms {
		if i > 0 {
			env.writeString(" OR ")
			env.space = false
		}
		for _, name := range slices.Sorted(maps.Keys(term.keys)) {
		for _, name := range maps.Keys(term.keys) {
			if env.space {
				env.writeString(" AND ")
			}
			env.writeString(name)
			switch term.keys[name] {
			case cmpExist:
				env.writeString(" EXIST")
			case cmpNotExist:
				env.writeString(" NOT EXIST")
			default:
				env.writeString(" IS SCHRÖDINGER'S CAT")
			}
			env.space = true
		}
		for _, name := range slices.Sorted(maps.Keys(term.mvals)) {
		for _, name := range maps.Keys(term.mvals) {
			if env.space {
				env.writeString(" AND ")
			}
			env.writeString(name)
			env.printHumanSelectExprValues(term.mvals[name])
			env.space = true
		}
253
254
255
256
257
258
259
260

261
262
263
264
265
266
267
252
253
254
255
256
257
258

259
260
261
262
263
264
265
266







-
+







			pe.writeString(" NOT GREATER ")
		default:
			pe.writeString(" MaTcH ")
		}
		if val.value == "" {
			pe.writeString("NOTHING")
		} else {
			pe.writeString(string(val.value))
			pe.writeString(val.value)
		}
	}
}

func (pe *PrintEnv) printOrder(order []sortOrder) {
	for _, o := range order {
		if o.isRandom() {

Changes to query/query.go.

15
16
17
18
19
20
21
22

23
24

25
26
27
28
29
30
31

32
33
34
35

36
37
38
39

40
41
42
43

44
45
46
47
48
49
50
15
16
17
18
19
20
21

22


23
24
25
26
27
28
29

30
31
32
33

34
35
36
37

38
39
40
41

42
43
44
45
46
47
48
49







-
+
-
-
+






-
+



-
+



-
+



-
+







package query

import (
	"context"
	"math/rand/v2"
	"slices"

	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/zettel/id"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {
	// Select all zettel that contains the given exact word.
	// The word must be normalized through Unicode NKFD, trimmed and not empty.
	SearchEqual(word string) *idset.Set
	SearchEqual(word string) *id.Set

	// Select all zettel that have a word with the given prefix.
	// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchPrefix(prefix string) *idset.Set
	SearchPrefix(prefix string) *id.Set

	// Select all zettel that have a word with the given suffix.
	// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchSuffix(suffix string) *idset.Set
	SearchSuffix(suffix string) *id.Set

	// Select all zettel that contains the given string.
	// The string must be normalized through Unicode NKFD, trimmed and not empty.
	SearchContains(s string) *idset.Set
	SearchContains(s string) *id.Set
}

// Query specifies a mechanism for querying zettel.
type Query struct {
	// Präfixed zettel identifier.
	zids []id.Zid

70
71
72
73
74
75
76


77

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

78
79
80
81
82
83
84
85







+
+
-
+







}

// GetZids returns a slide of all specified zettel identifier.
func (q *Query) GetZids() []id.Zid {
	if q == nil || len(q.zids) == 0 {
		return nil
	}
	result := make([]id.Zid, len(q.zids))
	copy(result, q.zids)
	return slices.Clone(q.zids)
	return result
}

// Directive are executed to process the list of metadata.
type Directive interface {
	Print(*PrintEnv)
}

137
138
139
140
141
142
143

144
145







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


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







+
-
-
+
+
+
+
+
+
+








// Clone the query value.
func (q *Query) Clone() *Query {
	if q == nil {
		return nil
	}
	c := new(Query)
	if len(q.zids) > 0 {
	c.zids = q.GetZids()
	c.directives = q.GetDirectives()
		c.zids = make([]id.Zid, len(q.zids))
		copy(c.zids, q.zids)
	}
	if len(q.directives) > 0 {
		c.directives = make([]Directive, len(q.directives))
		copy(c.directives, q.directives)
	}

	c.preMatch = q.preMatch
	c.terms = make([]conjTerms, len(q.terms))
	for i, term := range q.terms {
		if len(term.keys) > 0 {
			c.terms[i].keys = make(keyExistMap, len(term.keys))
			for k, v := range term.keys {
227
228
229
230
231
232
233
234

235
236
237
238
239
240
241
234
235
236
237
238
239
240

241
242
243
244
245
246
247
248







-
+







	cmpNoLess:    true,
	cmpNoGreater: true,
}

func (op compareOp) isNegated() bool { return negativeMap[op] }

type expValue struct {
	value meta.Value
	value string
	op    compareOp
}

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)
249
250
251
252
253
254
255
256

257
258
259
260
261
262
263
256
257
258
259
260
261
262

263
264
265
266
267
268
269
270







-
+







	cmpHasNot:   true,
	cmpNoMatch:  true,
}

// GetMetaValues returns the slice of all values specified for a given metadata key.
// If `withMissing` is true, all values are returned. Otherwise only those,
// where the comparison operator will positively search for a value.
func (q *Query) GetMetaValues(key string, withMissing bool) (vals []meta.Value) {
func (q *Query) GetMetaValues(key string, withMissing bool) (vals []string) {
	if q == nil {
		return nil
	}
	for _, term := range q.terms {
		if mvs, hasMv := term.mvals[key]; hasMv {
			for _, ev := range mvs {
				if withMissing || !missingMap[ev.op] {
325
326
327
328
329
330
331
332

333
334
335
336
337
338
339
332
333
334
335
336
337
338

339
340
341
342
343
344
345
346







-
+







	if q == nil {
		return false
	}
	if len(q.zids) > 0 {
		return true
	}
	if len(q.actions) > 0 {
		// Unknown, what an action may use. For examples: KEYS action uses all metadata.
		// Unknown, what an action will use. Example: RSS needs api.KeyPublished.
		return true
	}
	for _, term := range q.terms {
		for key := range term.keys {
			if meta.IsProperty(key) {
				return true
			}
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
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







-
+



-
+






-
+








-
+







			cTerm.Match = matchAlways
		}
		result.Terms = append(result.Terms, cTerm)
	}
	return result
}

func metaList2idSet(ml []*meta.Meta) *idset.Set {
func metaList2idSet(ml []*meta.Meta) *id.Set {
	if ml == nil {
		return nil
	}
	result := idset.NewCap(len(ml))
	result := id.NewSetCap(len(ml))
	for _, m := range ml {
		result = result.Add(m.Zid)
	}
	return result
}

func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet *idset.Set) CompiledTerm {
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 := idset.NewCap(startSet.Length())
				predSet := id.NewSetCap(startSet.Length())
				startSet.ForEach(func(zid id.Zid) {
					if pred(zid) {
						predSet = predSet.Add(zid)
					}
				})
				pred = predSet.ContainsOrNil
			}

Changes to query/retrieve.go.

15
16
17
18
19
20
21
22
23


24
25
26
27
28
29
30

31
32
33
34
35
36
37
15
16
17
18
19
20
21


22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37







-
-
+
+






-
+








// This file contains helper functions to search within the index.

import (
	"fmt"
	"strings"

	"t73f.de/r/zsc/domain/id/idset"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type searchOp struct {
	s  string
	op compareOp
}
type searchFunc func(string) *idset.Set
type searchFunc func(string) *id.Set
type searchCallMap map[searchOp]searchFunc

var cmpPred = map[compareOp]func(string, string) bool{
	cmpEqual:   stringEqual,
	cmpPrefix:  strings.HasPrefix,
	cmpSuffix:  strings.HasSuffix,
	cmpMatch:   strings.Contains,
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
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







-
+











-
+


-
+

-
+

















-
+

-
+






-
-
-
+
+
+



+
+
+




-
+

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













-
-
+
+







	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(string(val.value)) {
		for _, word := range strfun.NormalizeWords(val.value) {
			if cmpOp := val.op; cmpOp.isNegated() {
				cmpOp = cmpOp.negate()
				negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
			} else {
				normCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
			}
		}
	}

	plainCalls = make(searchCallMap, len(search))
	for _, val := range search {
		word := val.value.TrimSpace().ToLower()
		word := strings.ToLower(strings.TrimSpace(val.value))
		if cmpOp := val.op; cmpOp.isNegated() {
			cmpOp = cmpOp.negate()
			negCalls.addSearch(string(word), cmpOp, getSearchFunc(searcher, cmpOp))
			negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
		} else {
			plainCalls.addSearch(string(word), cmpOp, getSearchFunc(searcher, cmpOp))
			plainCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp))
		}
	}
	return normCalls, plainCalls, negCalls
}

func hasConflictingCalls(normCalls, plainCalls, negCalls searchCallMap) bool {
	for val := range negCalls {
		if _, found := normCalls[val]; found {
			return true
		}
		if _, found := plainCalls[val]; found {
			return true
		}
	}
	return false
}

func retrievePositives(normCalls, plainCalls searchCallMap) *idset.Set {
func retrievePositives(normCalls, plainCalls searchCallMap) *id.Set {
	if isSuperset(normCalls, plainCalls) {
		var normResult *idset.Set
		var normResult *id.Set
		for c, sf := range normCalls {
			normResult = normResult.IntersectOrSet(sf(c.s))
		}
		return normResult
	}

	cache := make(map[searchOp]*idset.Set)

	var plainResult *idset.Set
	type searchResults map[searchOp]*id.Set
	var cache searchResults
	var plainResult *id.Set
	for c, sf := range plainCalls {
		result := sf(c.s)
		if _, found := normCalls[c]; found {
			if cache == nil {
				cache = make(searchResults)
			}
			cache[c] = result
		}
		plainResult = plainResult.IntersectOrSet(result)
	}
	var normResult *idset.Set
	var normResult *id.Set
	for c, sf := range normCalls {
		if cache != nil {
		if result, found := cache[c]; found {
			normResult = normResult.IntersectOrSet(result)
			if result, found := cache[c]; found {
				normResult = normResult.IntersectOrSet(result)
		} else {
			normResult = normResult.IntersectOrSet(sf(c.s))
		}
				continue
			}
		}
		normResult = normResult.IntersectOrSet(sf(c.s))
	}
	return normResult.IUnion(plainResult)
}

func isSuperset(normCalls, plainCalls searchCallMap) bool {
	for c := range plainCalls {
		if _, found := normCalls[c]; !found {
			return false
		}
	}
	return true
}

func retrieveNegatives(negCalls searchCallMap) *idset.Set {
	var negatives *idset.Set
func retrieveNegatives(negCalls searchCallMap) *id.Set {
	var negatives *id.Set
	for val, sf := range negCalls {
		negatives = negatives.IUnion(sf(val.s))
	}
	return negatives
}

func getSearchFunc(searcher Searcher, op compareOp) searchFunc {

Changes to query/select.go.

13
14
15
16
17
18
19

20

21



22
23
24

25
26

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

23
24
25
26
27

28
29

30
31
32
33
34
35
36
37







+

+
-
+
+
+


-
+

-
+








package query

import (
	"fmt"
	"strconv"
	"strings"
	"unicode/utf8"

	"zettelstore.de/z/encoder/textenc"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

type matchValueFunc func(value meta.Value) bool
type matchValueFunc func(value string) bool

func matchValueNever(meta.Value) bool { return false }
func matchValueNever(string) bool { return false }

type matchSpec struct {
	key   string
	match matchValueFunc
}

// compileMeta calculates a selection func based on the given select criteria.
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

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


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


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

174
175
176
177
178
179
180
181
182
183
184
185


186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204

205
206
207
208
209
210
211
212
213
214
215
216
217


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


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

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

328
329
330
331
332

333
334
335
336
337
338
339
340
341
342

343
344
345
346
347
348
349
350
351
352
353

354
355
356
357


358
359
360
361
362
363
364
365
366







+
+






-
+











-
-
+
+












-
-
+
+











-
+











-
-
+
+

















-
+












-
-
+
+











-
-
+
+













-
+






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



-
+




-
+









-
+










-
+



-
-
+
+







		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.TypeZettelmarkup:
		return createMatchZmkFunc(values, addSearch)
	}
	return createMatchStringFunc(values, addSearch)
}

func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToIDPredicates(values, addSearch)
	return func(value meta.Value) bool {
	return func(value string) bool {
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := valuesToSetPredicates(preprocessSet(values), addSearch)
	return func(value meta.Value) bool {
		ids := value.AsSlice()
	return func(value string) bool {
		ids := meta.ListFromValue(value)
		for _, preds := range predList {
			for _, pred := range preds {
				if !pred(ids) {
					return false
				}
			}
		}
		return true
	}
}
func createMatchTimestampFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToTimestampPredicates(values, addSearch)
	return func(value meta.Value) bool {
		value = meta.Value(meta.ExpandTimestamp(value))
	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 meta.Value) bool {
	return func(value string) bool {
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := valuesToSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch)
	return func(value meta.Value) bool {
		tags := value.AsTags()
	return func(value string) bool {
		tags := meta.TagsFromValue(value)
		for _, preds := range predList {
			for _, pred := range preds {
				if !pred(tags) {
					return false
				}
			}
		}
		return true
	}
}

func processTagSet(valueSet [][]expValue) [][]expValue {
	result := make([][]expValue, len(valueSet))
	for i, values := range valueSet {
		tags := make([]expValue, len(values))
		for j, val := range values {
			if tval := val.value; tval != "" && tval[0] == '#' {
				tval = tval.CleanTag()
				tval = meta.CleanTag(tval)
				tags[j] = expValue{value: tval, op: val.op}
			} else {
				tags[j] = expValue{value: tval, op: val.op}
			}
		}
		result[i] = tags
	}
	return result
}

func createMatchWordFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToWordPredicates(sliceToLower(values), addSearch)
	return func(value meta.Value) bool {
		value = meta.Value(strings.ToLower(string(value)))
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToStringPredicates(sliceToLower(values), addSearch)
	return func(value meta.Value) bool {
		value = meta.Value(strings.ToLower(string(value)))
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func sliceToLower(sl []expValue) []expValue {
	result := make([]expValue, 0, len(sl))
	for _, s := range sl {
		result = append(result, expValue{
			value: meta.Value(strings.ToLower(string(s.value))),
			value: strings.ToLower(s.value),
			op:    s.op,
		})
	}
	return result
}

func createMatchZmkFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	normPreds := make([]stringPredicate, 0, len(values))
	negPreds := make([]stringPredicate, 0, len(values))
	for _, v := range values {
		for _, word := range strfun.NormalizeWords(v.value) {
			if cmpOp := v.op; cmpOp.isNegated() {
				cmpOp = cmpOp.negate()
				negPreds = append(negPreds, createWordCompareFunc(word, cmpOp))
			} else {
				normPreds = append(normPreds, createWordCompareFunc(word, cmpOp))
				addSearch(expValue{word, cmpOp}) // addSearch only for positive selections
			}
		}
	}
	return func(metaValue string) bool {
		temp := strings.Fields(zmk2text(metaValue))
		values := make([]string, 0, len(temp))
		for _, s := range temp {
			values = append(values, strfun.NormalizeWords(s)...)
		}
		for _, pred := range normPreds {
			if noneOf(pred, values) {
				return false
			}
		}
		for _, pred := range negPreds {
			for _, val := range values {
				if pred(val) {
					return false
				}
			}
		}
		return true
	}
}

func noneOf(pred stringPredicate, values []string) bool {
	for _, value := range values {
		if pred(value) {
			return false
		}
	}
	return true
}

func zmk2text(zmk string) string {
	isASCII, hasUpper, needParse := true, false, false
	for i := range len(zmk) {
		ch := zmk[i]
		if ch >= utf8.RuneSelf {
			isASCII = false
			break
		}
		hasUpper = hasUpper || ('A' <= ch && ch <= 'Z')
		needParse = needParse || !(('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') || ch == ' ')
	}
	if isASCII {
		if !needParse {
			if !hasUpper {
				return zmk
			}
			return strings.ToLower(zmk)
		}
	}
	is := parser.ParseMetadata(zmk)
	var sb strings.Builder
	if _, err := textenc.Create().WriteInlines(&sb, &is); err != nil {
		return strings.ToLower(zmk)
	}
	return strings.ToLower(sb.String())
}

func preprocessSet(set []expValue) [][]expValue {
	result := make([][]expValue, 0, len(set))
	for _, elem := range set {
		splitElems := strings.Split(string(elem.value), ",")
		splitElems := strings.Split(elem.value, ",")
		valueElems := make([]expValue, 0, len(splitElems))
		for _, se := range splitElems {
			e := strings.TrimSpace(se)
			if len(e) > 0 {
				valueElems = append(valueElems, expValue{value: meta.Value(e), op: elem.op})
				valueElems = append(valueElems, expValue{value: e, op: elem.op})
			}
		}
		if len(valueElems) > 0 {
			result = append(result, valueElems)
		}
	}
	return result
}

type stringPredicate func(meta.Value) bool
type stringPredicate func(string) bool

func valuesToIDPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		value := v.value
		if len(value) > 14 {
			value = value[:14]
		}
		switch op := disambiguatedIDOp(v.op); op {
		case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
			if isDigits(string(value)) {
			if isDigits(value) {
				// Never add the strValue to search.
				// Append enough zeroes to make it comparable as string.
				// (an ID and a timestamp always have 14 digits)
				strValue := string(value) + "00000000000000"[:14-len(value)]
				result[i] = createIDCompareFunc(meta.Value(strValue), op)
				strValue := value + "00000000000000"[:14-len(value)]
				result[i] = createIDCompareFunc(strValue, op)
				continue
			}
			fallthrough
		default:
			// Otherwise compare as a word.
			if !op.isNegated() {
				addSearch(v) // addSearch only for positive selections
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
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







-
+











-
+








-
+







-
+








-
+







		}
	}
	return true
}

func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createIDCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate {
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(meta.Value(value), op)
				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(meta.Value(value), op)
			result[i] = createWordCompareFunc(value, op)
		}
	}
	return result
}

func disambiguatedTimestampOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createTimestampCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate {
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(string(v.value), 10, 64)
			iValue, err := strconv.ParseInt(v.value, 10, 64)
			if err == nil {
				// Never add the strValue to search.
				result[i] = createNumberCompareFunc(iValue, op)
				continue
			}
			fallthrough
		default:
376
377
378
379
380
381
382
383
384


385
386
387
388
389
390
391
454
455
456
457
458
459
460


461
462
463
464
465
466
467
468
469







-
-
+
+







	case cmpGreater:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal > cmpVal }
	case cmpNoGreater:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal <= cmpVal }
	default:
		panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal))
	}
	return func(metaVal meta.Value) bool {
		iMetaVal, err := strconv.ParseInt(string(metaVal), 10, 64)
	return func(metaVal string) bool {
		iMetaVal, err := strconv.ParseInt(metaVal, 10, 64)
		if err != nil {
			return false
		}
		return cmpFunc(iMetaVal)
	}
}

408
409
410
411
412
413
414
415

416
417
418
419
420
421
422
486
487
488
489
490
491
492

493
494
495
496
497
498
499
500







-
+







	case cmpHasNot:
		return cmpNoMatch
	default:
		return cmpOp
	}
}

func createStringCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate {
func createStringCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	return createWordCompareFunc(cmpVal, cmpOp)
}

func valuesToWordPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		op := disambiguateWordOp(v.op)
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
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







-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+







	case cmpHasNot:
		return cmpNotEqual
	default:
		return cmpOp
	}
}

func createWordCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate {
func createWordCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	switch cmpOp {
	case cmpEqual:
		return func(metaVal meta.Value) bool { return metaVal == cmpVal }
		return func(metaVal string) bool { return metaVal == cmpVal }
	case cmpNotEqual:
		return func(metaVal meta.Value) bool { return metaVal != cmpVal }
		return func(metaVal string) bool { return metaVal != cmpVal }
	case cmpPrefix:
		return func(metaVal meta.Value) bool { return strings.HasPrefix(string(metaVal), string(cmpVal)) }
		return func(metaVal string) bool { return strings.HasPrefix(metaVal, cmpVal) }
	case cmpNoPrefix:
		return func(metaVal meta.Value) bool { return !strings.HasPrefix(string(metaVal), string(cmpVal)) }
		return func(metaVal string) bool { return !strings.HasPrefix(metaVal, cmpVal) }
	case cmpSuffix:
		return func(metaVal meta.Value) bool { return strings.HasSuffix(string(metaVal), string(cmpVal)) }
		return func(metaVal string) bool { return strings.HasSuffix(metaVal, cmpVal) }
	case cmpNoSuffix:
		return func(metaVal meta.Value) bool { return !strings.HasSuffix(string(metaVal), string(cmpVal)) }
		return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) }
	case cmpMatch:
		return func(metaVal meta.Value) bool { return strings.Contains(string(metaVal), string(cmpVal)) }
		return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) }
	case cmpNoMatch:
		return func(metaVal meta.Value) bool { return !strings.Contains(string(metaVal), string(cmpVal)) }
		return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) }
	case cmpLess:
		return func(metaVal meta.Value) bool { return metaVal < cmpVal }
		return func(metaVal string) bool { return metaVal < cmpVal }
	case cmpNoLess:
		return func(metaVal meta.Value) bool { return metaVal >= cmpVal }
		return func(metaVal string) bool { return metaVal >= cmpVal }
	case cmpGreater:
		return func(metaVal meta.Value) bool { return metaVal > cmpVal }
		return func(metaVal string) bool { return metaVal > cmpVal }
	case cmpNoGreater:
		return func(metaVal meta.Value) bool { return metaVal <= cmpVal }
		return func(metaVal string) bool { return metaVal <= cmpVal }
	case cmpHas, cmpHasNot:
		panic(fmt.Sprintf("operator %d not disambiguated with value %q", cmpOp, cmpVal))
	default:
		panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal))
	}
}

518
519
520
521
522
523
524
525

526
527
528

529
530
531
532
533
534
535
596
597
598
599
600
601
602

603
604
605

606
607
608
609
610
611
612
613







-
+


-
+








func stringEqual(val1, val2 string) bool   { return val1 == val2 }
func stringLess(val1, val2 string) bool    { return val1 < val2 }
func stringGreater(val1, val2 string) bool { return val1 > val2 }

type compareStringFunc func(val1, val2 string) bool

func makeStringSetPredicate(neededValue meta.Value, compare compareStringFunc, foundResult bool) stringSetPredicate {
func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate {
	return func(metaVals []string) bool {
		for _, metaVal := range metaVals {
			if compare(metaVal, string(neededValue)) {
			if compare(metaVal, neededValue) {
				return foundResult
			}
		}
		return !foundResult
	}
}

Changes to query/select_test.go.

14
15
16
17
18
19
20
21
22
23



24
25
26
27
28

29
30
31
32

33
34
35
36
37



38
39
40

41
42
43
44
45
46
47
14
15
16
17
18
19
20



21
22
23
24
25
26


27
28
29
30

31
32
33



34
35
36
37
38

39
40
41
42
43
44
45
46







-
-
-
+
+
+



-
-
+



-
+


-
-
-
+
+
+


-
+







package query_test

import (
	"context"
	"testing"

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

func TestMatchZidNegate(t *testing.T) {
	q := query.Parse(meta.KeyID + api.SearchOperatorHasNot + id.ZidVersion.String() + " " +
		meta.KeyID + api.SearchOperatorHasNot + id.ZidLicense.String())
	q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense))
	compiled := q.RetrieveAndCompile(context.Background(), nil, nil)

	testCases := []struct {
		zid id.Zid
		zid api.ZettelID
		exp bool
	}{
		{id.ZidVersion, false},
		{id.ZidLicense, false},
		{id.ZidAuthors, true},
		{api.ZidVersion, false},
		{api.ZidLicense, false},
		{api.ZidAuthors, true},
	}
	for i, tc := range testCases {
		m := meta.New(tc.zid)
		m := meta.New(id.MustParse(tc.zid))
		if compiled.Terms[0].Match(m) != tc.exp {
			if tc.exp {
				t.Errorf("%d: meta %v must match %q", i, m.Zid, q)
			} else {
				t.Errorf("%d: meta %v must not match %q", i, m.Zid, q)
			}
		}

Changes to query/sorter.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
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38







-
+
+









-
+








package query

import (
	"cmp"
	"strconv"

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

type sortFunc func(i, j *meta.Meta) int

func buildSortFunc(order []sortOrder) sortFunc {
	hasID := false
	sortFuncs := make([]sortFunc, 0, len(order)+1)
	for _, o := range order {
		sortFuncs = append(sortFuncs, o.buildSortfunc())
		if o.key == meta.KeyID {
		if o.key == api.KeyID {
			hasID = true
			break
		}
	}
	if !hasID {
		sortFuncs = append(sortFuncs, defaultMetaSort)
	}
47
48
49
50
51
52
53
54

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

55
56
57
58
59
60
61
62







-
+







		return 0
	}
}

func (so *sortOrder) buildSortfunc() sortFunc {
	key := so.key
	keyType := meta.Type(key)
	if key == meta.KeyID || keyType == meta.TypeCredential {
	if key == api.KeyID || keyType == meta.TypeCredential {
		if so.descending {
			return defaultMetaSort
		}
		return func(i, j *meta.Meta) int { return cmp.Compare(i.Zid, j.Zid) }
	}
	if keyType == meta.TypeTimestamp {
		return createSortTimestampFunc(key, so.descending)
108
109
110
111
112
113
114
115

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

116
117
118
119
120
121
122
123







-
+







		}
		return cmp.Compare(iVal, jVal)
	}
}

func getNum(m *meta.Meta, key string) (int64, bool) {
	if s, ok := m.Get(key); ok {
		if i, err := strconv.ParseInt(string(s), 10, 64); err == nil {
		if i, err := strconv.ParseInt(s, 10, 64); err == nil {
			return i, true
		}
	}
	return 0, false
}

func createSortStringFunc(key string, descending bool) sortFunc {

Changes to query/unlinked.go.

11
12
13
14
15
16
17
18
19


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


18
19
20
21
22
23
24
25
26







-
-
+
+







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

package query

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

// UnlinkedSpec contains all specification values to calculate unlinked references.
type UnlinkedSpec struct {
	words []string
}

38
39
40
41
42
43
44
45

46
47
48
49

50
51
52
38
39
40
41
42
43
44

45
46
47
48

49
50
51
52







-
+



-
+



	if words := spec.words; len(words) > 0 {
		result := make([]string, len(words))
		copy(result, words)
		return result
	}
	result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title
	for _, m := range metaSeq {
		title, hasTitle := m.Get(meta.KeyTitle)
		title, hasTitle := m.Get(api.KeyTitle)
		if !hasTitle {
			continue
		}
		result = append(result, strfun.MakeWords(string(title))...)
		result = append(result, strfun.MakeWords(title)...)
	}
	return result
}

Added strfun/set.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package strfun

// Set ist a set of strings.
type Set map[string]struct{}

// NewSet creates a new set from the given values.
func NewSet(values ...string) Set {
	s := make(Set, len(values))
	for _, v := range values {
		s.Set(v)
	}
	return s
}

// Set adds the given string to the set.
func (s Set) Set(v string) { s[v] = struct{}{} }

// Has returns true, if given value is in set.
func (s Set) Has(v string) bool { _, found := s[v]; return found }

Changes to tests/client/client_test.go.

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







36
37
38
39

40
41

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


55
56
57
58


59
60
61
62
63
64
65
23
24
25
26
27
28
29


30
31
32

33
34
35
36
37
38
39
40
41
42

43
44

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


57
58
59
60


61
62
63
64
65
66
67
68
69







-
-



-
+
+
+
+
+
+
+



-
+

-
+











-
-
+
+


-
-
+
+







	"net/url"
	"slices"
	"strconv"
	"testing"

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

func nextZid(zid id.Zid) id.Zid { return zid + 1 }
func nextZid(zid api.ZettelID) api.ZettelID {
	numVal, err := strconv.ParseUint(string(zid), 10, 64)
	if err != nil {
		panic(err)
	}
	return api.ZettelID(fmt.Sprintf("%014d", numVal+1))
}

func TestNextZid(t *testing.T) {
	testCases := []struct {
		zid, exp id.Zid
		zid, exp api.ZettelID
	}{
		{1, 2},
		{api.ZettelID("00000000000000"), api.ZettelID("00000000000001")},
	}
	for i, tc := range testCases {
		if got := nextZid(tc.zid); got != tc.exp {
			t.Errorf("%d: zid=%q, exp=%q, got=%q", i, tc.zid, tc.exp, got)
		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 58
		configRoleZettel = 36
		ownerZettel      = 57
		configRoleZettel = 35
		writerZettel     = ownerZettel - 25
		readerZettel     = ownerZettel - 25
		creatorZettel    = 11
		publicZettel     = 6
		creatorZettel    = 10
		publicZettel     = 5
	)

	testdata := []struct {
		user string
		exp  int
	}{
		{"", publicZettel},
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
91
92
93
94
95
96
97

98
99
100
101
102
103
104
105







-
+







			}
			got := len(l)
			if got != tc.exp {
				tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
			}
		})
	}
	search := meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleConfiguration + " ORDER id"
	search := api.KeyRole + api.SearchOperatorHas + api.ValueRoleConfiguration + " ORDER id"
	q, h, l, err := c.QueryZettelData(context.Background(), search)
	if err != nil {
		t.Error(err)
		return
	}
	expQ := "role:configuration ORDER id"
	if q != expQ {
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
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







-
-
+










-
+











-
+








func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) {
	t.Helper()
	if len(pl) != len(l) {
		t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l))
	} else {
		for i, line := range pl {
			got, err := id.Parse(string(line[:14]))
			if err == nil && got != l[i].ID {
			if got := api.ZettelID(line[:14]); got != l[i].ID {
				t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got)
			}
		}
	}
}

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

	mr, err := c.GetMetaData(context.Background(), id.ZidDefaultHome)
	mr, err := c.GetMetaData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if mr.Rights == api.ZettelCanNone {
		t.Error("rights must be greater zero")
	}
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
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







-
+







-
+










-
+

-
+








-
+








-
-
-
-
+
+
+
+




-
-
-
-
-
+
+
+
+
+





-
+




-
+

-
+




-
+

-
+






-
-
+









-
+







	c.SetAuth("owner", "owner")
	encodings := []api.EncodingEnum{
		api.EncoderHTML,
		api.EncoderSz,
		api.EncoderText,
	}
	for _, enc := range encodings {
		content, err := c.GetParsedZettel(context.Background(), id.ZidDefaultHome, enc)
		content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for parsed encoding %v", enc)
		}
		content, err = c.GetEvaluatedZettel(context.Background(), id.ZidDefaultHome, enc)
		content, err = c.GetEvaluatedZettel(context.Background(), api.ZidDefaultHome, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for evaluated encoding %v", enc)
		}
	}
}

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

func TestGetZettelOrder(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), id.ZidTOCNewTemplate.String()+" "+api.ItemsDirective)
	_, _, 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, id.ZidTemplateNewZettel)
	checkListZid(t, metaSeq, 1, id.ZidTemplateNewRole)
	checkListZid(t, metaSeq, 2, id.ZidTemplateNewTag)
	checkListZid(t, metaSeq, 3, id.ZidTemplateNewUser)
	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 = id.Zid(20211019200500)
		ownerZid   = id.Zid(20210629163300)
		writerZid  = id.Zid(20210629165000)
		readerZid  = id.Zid(20210629165024)
		creatorZid = id.Zid(20210629165050)
		allUserZid = api.ZettelID("20211019200500")
		ownerZid   = api.ZettelID("20210629163300")
		writerZid  = api.ZettelID("20210629165000")
		readerZid  = api.ZettelID("20210629165024")
		creatorZid = api.ZettelID("20210629165050")
		limitAll   = 3
	)
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.QueryZettel(context.Background(), ownerZid.String()+" CONTEXT LIMIT "+strconv.Itoa(limitAll))
	rl, err := c.QueryZettel(context.Background(), string(ownerZid)+" CONTEXT LIMIT "+strconv.Itoa(limitAll))
	if err != nil {
		t.Error(err)
		return
	}
	checkZidList(t, []id.Zid{ownerZid, allUserZid, writerZid}, rl)
	checkZidList(t, []api.ZettelID{ownerZid, allUserZid, writerZid}, rl)

	rl, err = c.QueryZettel(context.Background(), ownerZid.String()+" CONTEXT BACKWARD")
	rl, err = c.QueryZettel(context.Background(), string(ownerZid)+" CONTEXT BACKWARD")
	if err != nil {
		t.Error(err)
		return
	}
	checkZidList(t, []id.Zid{ownerZid, allUserZid}, rl)
	checkZidList(t, []api.ZettelID{ownerZid, allUserZid}, rl)
}
func checkZidList(t *testing.T, exp []id.Zid, got [][]byte) {
func checkZidList(t *testing.T, exp []api.ZettelID, got [][]byte) {
	t.Helper()
	if len(exp) != len(got) {
		t.Errorf("expected a list fo length %d, but got %d", len(exp), len(got))
		return
	}
	for i, expZid := range exp {
		gotZid, err := id.Parse(string(got[i][:14]))
		if err != nil || expZid != gotZid {
		if gotZid := api.ZettelID(got[i][:14]); expZid != gotZid {
			t.Errorf("lists differ at pos %d: expected id %v, but got %v", i, expZid, gotZid)
		}
	}
}

func TestGetUnlinkedReferences(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), id.ZidDefaultHome.String()+" "+api.UnlinkedDirective)
	_, _, 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
318
319
320
321
322
323
324
325

326
327
328
329
330
331
332
320
321
322
323
324
325
326

327
328
329
330
331
332
333
334







-
+







	}
}

func TestListTags(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+meta.KeyTags)
	agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyTags)
	if err != nil {
		t.Error(err)
		return
	}
	tags := []struct {
		key  string
		size int
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
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







-
+



-
+











-
+







	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 != id.Invalid {
	} else if zid != "" {
		t.Errorf("no zid expected, but got %q", zid)
	}
	zid, err = c.TagZettel(ctx, "#test")
	exp := id.Zid(20230929102100)
	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+meta.KeyRole)
	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)
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
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







-
+



-
+










-
+












-
+







	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 != id.Invalid {
	} else if zid != "" {
		t.Errorf("no zid expected, but got %q", zid)
	}
	zid, err = c.RoleZettel(ctx, "zettel")
	exp := id.Zid(60010)
	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 TestRedirect(t *testing.T) {
	t.Parallel()
	c := getClient()
	search := "emoji" + api.ActionSeparator + api.RedirectAction
	search := api.OrderDirective + " " + api.ReverseDirective + " " + api.KeyID + api.ActionSeparator + api.RedirectAction
	ub := c.NewURLBuilder('z').AppendQuery(search)
	respRedirect, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respRedirect.Body.Close()
	bodyRedirect, err := io.ReadAll(respRedirect.Body)
	if err != nil {
		t.Error(err)
		return
	}
	ub.ClearQuery().SetZid(id.ZidEmoji)
	ub.ClearQuery().SetZid(api.ZidEmoji)
	respEmoji, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respEmoji.Body.Close()
	bodyEmoji, err := io.ReadAll(respEmoji.Body)
466
467
468
469
470
471
472
473
474


475
476
477
478
479
480
481
468
469
470
471
472
473
474


475
476
477
478
479
480
481
482
483







-
-
+
+







	c := getClient()
	c.SetAuth("reader", "reader")
	zid, err := c.GetApplicationZid(context.Background(), "app")
	if err != nil {
		t.Error(err)
		return
	}
	if zid != id.ZidAppDirectory {
		t.Errorf("c.GetApplicationZid(\"app\") should result in %q, but got: %q", id.ZidAppDirectory, zid)
	if zid != api.ZidAppDirectory {
		t.Errorf("c.GetApplicationZid(\"app\") should result in %q, but got: %q", api.ZidAppDirectory, zid)
	}
	if zid, err = c.GetApplicationZid(context.Background(), "noappzid"); err == nil {
		t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid)
	}
	if zid, err = c.GetApplicationZid(context.Background(), "nozid"); err == nil {
		t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid)
	}

Changes to tests/client/crud_test.go.

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


23
24
25
26
27
28
29







-
-







import (
	"context"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
)

// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.

func TestCreateGetDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
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
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







-
-
+
+











-
+


-
+









-
+













-
+




-
+








-
+





-
+




-
+




-
+

-
-
+
+




-
+




-
+


-
+





-
+


-
+






func TestCreateGetDeleteZettelData(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("owner", "owner")
	wrongModified := "19691231115959"
	zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
		Meta: api.ZettelMeta{
			meta.KeyTitle:    "A\nTitle", // \n must be converted into a space
			meta.KeyModified: wrongModified,
			api.KeyTitle:    "A\nTitle", // \n must be converted into a space
			api.KeyModified: wrongModified,
		},
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	z, err := c.GetZettelData(context.Background(), zid)
	if err != nil {
		t.Error("Cannot get zettel:", zid, err)
	} else {
		exp := "A Title"
		if got := z.Meta[meta.KeyTitle]; got != exp {
		if got := z.Meta[api.KeyTitle]; got != exp {
			t.Errorf("Expected title %q, but got %q", exp, got)
		}
		if got := z.Meta[meta.KeyModified]; got != "" {
		if got := z.Meta[api.KeyModified]; got != "" {
			t.Errorf("Create allowed to set the modified key: %q", got)
		}
	}
	doDelete(t, c, zid)
}

func TestUpdateZettel(t *testing.T) {
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettel(context.Background(), id.ZidDefaultHome, api.PartZettel)
	z, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel)
	if err != nil {
		t.Error(err)
		return
	}
	if !strings.HasPrefix(string(z), "title: Home\n") {
		t.Error("Got unexpected zettel", z)
		return
	}
	newZettel := `title: Empty Home
role: zettel
syntax: zmk

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

func TestUpdateZettelData(t *testing.T) {
	c := getClient()
	c.SetAuth("writer", "writer")
	z, err := c.GetZettelData(context.Background(), id.ZidDefaultHome)
	z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if got := z.Meta[meta.KeyTitle]; got != "Home" {
	if got := z.Meta[api.KeyTitle]; got != "Home" {
		t.Errorf("Title of zettel is not \"Home\", but %q", got)
		return
	}
	newTitle := "New Home"
	z.Meta[meta.KeyTitle] = newTitle
	z.Meta[api.KeyTitle] = newTitle
	wrongModified := "19691231235959"
	z.Meta[meta.KeyModified] = wrongModified
	err = c.UpdateZettelData(context.Background(), id.ZidDefaultHome, z)
	z.Meta[api.KeyModified] = wrongModified
	err = c.UpdateZettelData(context.Background(), api.ZidDefaultHome, z)
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettelData(context.Background(), id.ZidDefaultHome)
	zt, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if got := zt.Meta[meta.KeyTitle]; got != newTitle {
	if got := zt.Meta[api.KeyTitle]; got != newTitle {
		t.Errorf("Title of zettel is not %q, but %q", newTitle, got)
	}
	if got := zt.Meta[meta.KeyModified]; got == wrongModified {
	if got := zt.Meta[api.KeyModified]; got == wrongModified {
		t.Errorf("Update did not change the modified key: %q", got)
	}

	// Must delete to clean up for next tests
	c.SetAuth("owner", "owner")
	doDelete(t, c, id.ZidDefaultHome)
	doDelete(t, c, api.ZidDefaultHome)
}

func doDelete(t *testing.T, c *client.Client, zid id.Zid) {
func doDelete(t *testing.T, c *client.Client, zid api.ZettelID) {
	err := c.DeleteZettel(context.Background(), zid)
	if err != nil {
		t.Helper()
		t.Error("Cannot delete", zid, ":", err)
	}
}

Changes to tests/client/embed_test.go.

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


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






41
42
43
44
45
46
47
15
16
17
18
19
20
21

22
23
24


25
26
27
28
29
30
31
32
33






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







-



-
-
+
+







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








import (
	"context"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
)

const (
	abcZid   = id.Zid(20211020121000)
	abc10Zid = id.Zid(20211020121100)
	abcZid   = api.ZettelID("20211020121000")
	abc10Zid = api.ZettelID("20211020121100")
)

func TestZettelTransclusion(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const abc10000Zid = id.Zid(20211020121400)
	contentMap := map[id.Zid]int{
		abcZid:                 1,
		abc10Zid:               10,
		id.Zid(20211020121145): 100,
		id.Zid(20211020121300): 1000,
	const abc10000Zid = api.ZettelID("20211020121400")
	contentMap := map[api.ZettelID]int{
		abcZid:                         1,
		abc10Zid:                       10,
		api.ZettelID("20211020121145"): 100,
		api.ZettelID("20211020121300"): 1000,
	}
	content, err := c.GetZettel(context.Background(), abcZid, api.PartContent)
	if err != nil {
		t.Error(err)
		return
	}
	baseContent := string(content)
77
78
79
80
81
82
83
84

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

83
84
85
86
87
88
89
90







-
+







}

func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("reader", "reader")

	zettelData, err := c.GetZettelData(context.Background(), id.ZidEmoji)
	zettelData, err := c.GetZettelData(context.Background(), api.ZidEmoji)
	if err != nil {
		t.Error(err)
		return
	}
	expectedEnc := "base64"
	if got := zettelData.Encoding; expectedEnc != got {
		t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got)
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
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







-
-
-
+
+
+

-
+












-
+








-
-
+
+








-
+







-
+








-
+






func TestRecursiveTransclusion(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const (
		selfRecursiveZid      = id.Zid(20211020182600)
		indirectRecursive1Zid = id.Zid(20211020183700)
		indirectRecursive2Zid = id.Zid(20211020183800)
		selfRecursiveZid      = api.ZettelID("20211020182600")
		indirectRecursive1Zid = api.ZettelID("20211020183700")
		indirectRecursive2Zid = api.ZettelID("20211020183800")
	)
	recursiveZettel := map[id.Zid]id.Zid{
	recursiveZettel := map[api.ZettelID]api.ZettelID{
		selfRecursiveZid:      selfRecursiveZid,
		indirectRecursive1Zid: indirectRecursive2Zid,
		indirectRecursive2Zid: indirectRecursive1Zid,
	}
	for zid, errZid := range recursiveZettel {
		content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML)
		if err != nil {
			t.Error(err)
			continue
		}
		sContent := string(content)
		checkContentContains(t, zid, sContent, "Recursive transclusion")
		checkContentContains(t, zid, sContent, errZid.String())
		checkContentContains(t, zid, sContent, string(errZid))
	}
}
func TestNothingToTransclude(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const (
		transZid = id.Zid(20211020184342)
		emptyZid = id.Zid(20211020184300)
		transZid = api.ZettelID("20211020184342")
		emptyZid = api.ZettelID("20211020184300")
	)
	content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	sContent := string(content)
	checkContentContains(t, transZid, sContent, "<!-- Nothing to transclude")
	checkContentContains(t, transZid, sContent, emptyZid.String())
	checkContentContains(t, transZid, sContent, string(emptyZid))
}

func TestSelfEmbedRef(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const selfEmbedZid = id.Zid(20211020185400)
	const selfEmbedZid = api.ZettelID("20211020185400")
	content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	checkContentContains(t, selfEmbedZid, string(content), "Self embed reference")
}

func checkContentContains(t *testing.T, zid id.Zid, content, expected string) {
func checkContentContains(t *testing.T, zid api.ZettelID, content, expected string) {
	if !strings.Contains(content, expected) {
		t.Helper()
		t.Errorf("Zettel %q should contain %q, but does not: %q", zid, expected, content)
	}
}

Changes to tests/markdown_test.go.

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

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

55
56
57
58
59
60
61
18
19
20
21
22
23
24

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

54
55
56
57
58
59
60
61







-













+















-
+







	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/mdenc"
	_ "zettelstore.de/z/encoder/shtmlenc"
	_ "zettelstore.de/z/encoder/szenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/parser"
	_ "zettelstore.de/z/parser/markdown"
	_ "zettelstore.de/z/parser/zettelmark"
	"zettelstore.de/z/zettel/meta"
)

type markdownTestCase struct {
	Markdown  string `json:"markdown"`
	HTML      string `json:"html"`
	Example   int    `json:"example"`
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	Section   string `json:"section"`
}

func TestEncoderAvailability(t *testing.T) {
	t.Parallel()
	encoderMissing := false
	for _, enc := range encodings {
		enc := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN})
		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")
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
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







-
+







-
+















-
+









-
+















-
+













		ast := createMDBlockSlice(tc.Markdown, config.NoHTML)
		testAllEncodings(t, tc, &ast)
		testZmkEncoding(t, tc, &ast)
	}
}

func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice {
	return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.ValueSyntaxMarkdown, hi)
	return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi)
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) {
			encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}).WriteBlocks(&sb, ast)
			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()

		testID = tc.Example*100 + 2
		secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &secondAst)
		gotSecond := buf.String()

		// if gotFirst != gotSecond {
		// 	st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond)
		// }

		testID = tc.Example*100 + 3
		thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &thirdAst)
		gotThird := buf.String()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
		}
	})
}

func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, "abc``<br>``{=\"html\"}def"},
		{`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.

16
17
18
19
20
21
22
23

24
25
26
27

28
29
30
31
32
33
34
16
17
18
19
20
21
22

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







-
+




+







import (
	"bufio"
	"io"
	"os"
	"path/filepath"
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	_ "zettelstore.de/z/cmd"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

// Test all parser / encoder with a list of "naughty strings", i.e. unusual strings
// that often crash software.

func getNaughtyStrings() (result []string, err error) {
	fpath := filepath.Join("..", "testdata", "naughty", "blns.txt")
54
55
56
57
58
59
60
61

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

62
63
64
65
66
67
68
69







-
+







		}
	}
	return result
}

func getAllEncoder() (result []encoder.Encoder) {
	for _, enc := range encoder.GetEncodings() {
		e := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN})
		e := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		result = append(result, e)
	}
	return result
}

func TestNaughtyStringParser(t *testing.T) {
	blns, err := getNaughtyStrings()
79
80
81
82
83
84
85

86
87
88
89




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







+




+
+
+
+





	encs := getAllEncoder()
	if len(encs) == 0 {
		t.Fatal("no encoder found")
	}
	for _, s := range blns {
		for _, pinfo := range pinfos {
			bs := pinfo.ParseBlocks(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name)
			is := pinfo.ParseInlines(input.NewInput([]byte(s)), pinfo.Name)
			for _, enc := range encs {
				_, err = enc.WriteBlocks(io.Discard, &bs)
				if err != nil {
					t.Error(err)
				}
				_, err = enc.WriteInlines(io.Discard, &is)
				if err != nil {
					t.Error(err)
				}
			}
		}
	}
}

Changes to tests/regression_test.go.

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

37
38
39
40
41
42
43
21
22
23
24
25
26
27

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







-








+







	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"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,
	api.EncoderSz,
120
121
122
123
124
125
126
127

128
129

130
131
132
133
134
135
136
120
121
122
123
124
125
126

127
128

129
130
131
132
133
134
135
136







-
+

-
+







	}
	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: meta.ValueLangEN}); enc != nil {
	if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}); enc != nil {
		var sf strings.Builder
		enc.WriteMeta(&sf, zn.Meta)
		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) {
166
167
168
169
170
171
172
173

174
175
176
177
178
179
180
166
167
168
169
170
171
172

173
174
175
176
177
178
179
180







-
+







func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta {
	return m
}
func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML }
func (*myConfig) GetListPageSize() int                     { return 0 }
func (*myConfig) GetSiteName() string                      { return "" }
func (*myConfig) GetYAMLHeader() bool                      { return false }
func (*myConfig) GetZettelFileSyntax() []meta.Value        { return nil }
func (*myConfig) GetZettelFileSyntax() []string            { return nil }

func (*myConfig) GetSimpleMode() bool                      { return false }
func (*myConfig) GetExpertMode() bool                      { return false }
func (*myConfig) GetVisibility(*meta.Meta) meta.Visibility { return meta.VisibilityPublic }
func (*myConfig) GetMaxTransclusions() int                 { return 1024 }

var testConfig = &myConfig{}

Changes to tools/build/build.go.

22
23
24
25
26
27
28
29

30
31
32
33


34
35
36
37
38
39
40
22
23
24
25
26
27
28

29

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







-
+
-



+
+







	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/tools"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func readVersionFile() (string, error) {
	content, err := os.ReadFile("VERSION")
	if err != nil {
		return "", err
	}
212
213
214
215
216
217
218
219

220
221
222
223
224
225
226
213
214
215
216
217
218
219

220
221
222
223
224
225
226
227







-
+








	data, err := io.ReadAll(manualFile)
	if err != nil {
		return err
	}
	inp := input.NewInput(data)
	m := meta.NewFromInput(id.MustParse(versionZid), inp)
	m.SetNow(meta.KeyModified)
	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

Changes to tools/htmllint/htmllint.go.

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

31
32
33
34
35
36
37







-







	"os"
	"regexp"
	"slices"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/tools"
)

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()

56
57
58
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
55
56
57
58
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







-
+
















-
-
+
+

-
+





-
+






-
+




















-
+


-
+




-
+





-
+







	}
	zids, perm := calculateZids(metaList)
	for _, kd := range keyDescr {
		msgCount := 0
		fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text)
		for _, zid := range zidsToUse(zids, perm, kd.sampleSize) {
			var nmsgs int
			nmsgs, err = validateHTML(client, kd.uc, zid)
			nmsgs, err = validateHTML(client, kd.uc, api.ZettelID(zid))
			if err != nil {
				fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err)
				msgCount++
			} else {
				msgCount += nmsgs
			}
		}
		if msgCount == 1 {
			fmt.Fprintln(os.Stderr, "==> found 1 possible issue")
		} else if msgCount > 1 {
			fmt.Fprintf(os.Stderr, "==> found %v possible issues\n", msgCount)
		}
	}
	return nil
}

func calculateZids(metaList []api.ZidMetaRights) ([]id.Zid, []int) {
	zids := make([]id.Zid, len(metaList))
func calculateZids(metaList []api.ZidMetaRights) ([]string, []int) {
	zids := make([]string, len(metaList))
	for i, m := range metaList {
		zids[i] = m.ID
		zids[i] = string(m.ID)
	}
	slices.Sort(zids)
	return zids, rand.Perm(len(metaList))
}

func zidsToUse(zids []id.Zid, perm []int, sampleSize int) []id.Zid {
func zidsToUse(zids []string, perm []int, sampleSize int) []string {
	if sampleSize < 0 || len(perm) <= sampleSize {
		return zids
	}
	if sampleSize == 0 {
		return nil
	}
	result := make([]id.Zid, sampleSize)
	result := make([]string, sampleSize)
	for i := range sampleSize {
		result[i] = zids[perm[i]]
	}
	slices.Sort(result)
	return result
}

var keyDescr = []struct {
	uc         urlCreator
	text       string
	sampleSize int
}{
	{getHTMLZettel, "zettel HTML encoding", -1},
	{createJustKey('h'), "zettel web view", -1},
	{createJustKey('i'), "zettel info view", -1},
	{createJustKey('e'), "zettel edit form", 100},
	{createJustKey('c'), "zettel create form", 10},
	{createJustKey('d'), "zettel delete dialog", 200},
}

type urlCreator func(*client.Client, id.Zid) *api.URLBuilder
type urlCreator func(*client.Client, api.ZettelID) *api.URLBuilder

func createJustKey(key byte) urlCreator {
	return func(c *client.Client, zid id.Zid) *api.URLBuilder {
	return func(c *client.Client, zid api.ZettelID) *api.URLBuilder {
		return c.NewURLBuilder(key).SetZid(zid)
	}
}

func getHTMLZettel(client *client.Client, zid id.Zid) *api.URLBuilder {
func getHTMLZettel(client *client.Client, zid api.ZettelID) *api.URLBuilder {
	return client.NewURLBuilder('z').SetZid(zid).
		AppendKVQuery(api.QueryKeyEncoding, api.EncodingHTML).
		AppendKVQuery(api.QueryKeyPart, api.PartZettel)
}

func validateHTML(client *client.Client, uc urlCreator, zid id.Zid) (int, error) {
func validateHTML(client *client.Client, uc urlCreator, zid api.ZettelID) (int, error) {
	ub := uc(client, zid)
	if tools.Verbose {
		fmt.Fprintf(os.Stderr, "GET %v\n", ub)
	}
	data, err := client.Get(context.Background(), ub)
	if err != nil {
		return 0, err

Changes to usecase/authenticate.go.

15
16
17
18
19
20
21
22

23
24
25
26


27
28
29
30
31
32
33
15
16
17
18
19
20
21

22

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







-
+
-



+
+








import (
	"context"
	"math/rand/v2"
	"net/http"
	"time"

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

// Authenticate is the data for this use case.
type Authenticate struct {
	log       *logger.Logger
	token     auth.TokenManager
	ucGetUser *GetUser
52
53
54
55
56
57
58
59
60


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


60
61
62
63
64
65
66
67
68







-
-
+
+








	if identMeta == nil || err != nil {
		uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found")
		compensateCompare()
		return nil, err
	}

	if hashCred, ok := identMeta.Get(meta.KeyCredential); ok {
		ok, err = cred.CompareHashAndCredential(string(hashCred), identMeta.Zid, ident, credential)
	if hashCred, ok := identMeta.Get(api.KeyCredential); ok {
		ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential)
		if err != nil {
			uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials")
			return nil, err
		}
		if ok {
			token, err2 := uc.token.GetToken(identMeta, d, k)
			if err2 != nil {

Changes to usecase/create_zettel.go.

13
14
15
16
17
18
19
20

21
22
23
24


25
26
27
28
29
30
31
13
14
15
16
17
18
19

20

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







-
+
-



+
+








package usecase

import (
	"context"
	"time"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/config"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// CreateZettelPort is the interface used by this use case.
type CreateZettelPort interface {
	// CreateZettel creates a new zettel.
	CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error)
}
46
47
48
49
50
51
52
53
54


55
56
57
58
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
47
48
49
50
51
52
53


54
55
56
57
58
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







-
-
+
+











-
+










-
-
+
+


-
+







-
-
+
+


-
+







-
+



-
-
-
+
+
+



-
+







-
-
-
+
+
+


-
+







-
+





-
+










-
-
+
+







	}
}

// 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(meta.KeyTitle); found {
		m.Set(meta.KeyTitle, prependTitle(title, "Copy", "Copy of "))
	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}
}

// PrepareVersion the zettel for further modification.
func (*CreateZettel) PrepareVersion(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := origMeta.Clone()
	m.Set(meta.KeyPredecessor, meta.Value(origMeta.Zid.String()))
	m.Set(api.KeyPredecessor, origMeta.Zid.String())
	setReadonly(m)
	content := origZettel.Content
	content.TrimSpace()
	return zettel.Zettel{Meta: m, Content: content}
}

// PrepareFolge the zettel for further modification.
func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(meta.KeyTitle); found {
		m.Set(meta.KeyTitle, prependTitle(title, "Folge", "Folge of "))
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(meta.KeyPrecursor, meta.Value(origMeta.Zid.String()))
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareSequel the zettel for further modification.
func (*CreateZettel) PrepareSequel(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(meta.KeyTitle); found {
		m.Set(meta.KeyTitle, prependTitle(title, "Sequel", "Sequel of "))
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Sequel", "Sequel of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(meta.KeyPrequel, meta.Value(origMeta.Zid.String()))
	m.Set(api.KeyPrequel, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta
	m.SetNonEmpty(meta.KeyTitle, om.GetDefault(meta.KeyTitle, ""))
	m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	updateMetaRoleTagsSyntax(m, om)

	const prefixLen = len(meta.NewPrefix)
	for key, val := range om.Rest() {
		if len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
			m.Set(key[prefixLen:], val)
	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(meta.KeyTitle, meta.Value(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(meta.KeyRole, orig.GetDefault(meta.KeyRole, ""))
	m.SetNonEmpty(meta.KeyTags, orig.GetDefault(meta.KeyTags, ""))
	m.SetNonEmpty(meta.KeySyntax, orig.GetDefault(meta.KeySyntax, meta.DefaultSyntax))
	m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, ""))
	m.SetNonEmpty(api.KeyTags, orig.GetDefault(api.KeyTags, ""))
	m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, meta.DefaultSyntax))
}

func prependTitle(title, s0, s1 meta.Value) meta.Value {
func prependTitle(title, s0, s1 string) string {
	if len(title) > 0 {
		return s1 + title
	}
	return s0
}

func setReadonly(m *meta.Meta) {
	if _, found := m.Get(meta.KeyReadOnly); found {
	if _, found := m.Get(api.KeyReadOnly); found {
		// Currently, "false" is a safe value.
		//
		// If the current user and its role is known, a more elaborative calculation
		// could be done: set it to a value, so that the current user will be able
		// to modify it later.
		m.Set(meta.KeyReadOnly, meta.ValueFalse)
		m.Set(api.KeyReadOnly, api.ValueFalse)
	}
}

// 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(meta.KeyCreated, meta.Value(time.Now().Local().Format(id.TimestampLayout)))
	m.Delete(meta.KeyModified)
	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
}

Changes to usecase/delete_zettel.go.

12
13
14
15
16
17
18
19
20


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


19
20
21
22
23
24
25
26
27







-
-
+
+







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

package usecase

import (
	"context"

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

// DeleteZettelPort is the interface used by this use case.
type DeleteZettelPort interface {
	// DeleteZettel removes the zettel from the box.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

Changes to usecase/evaluate.go.

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


27
28
29
30
31
32
33
12
13
14
15
16
17
18


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







-
-






+
+







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

package usecase

import (
	"context"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Evaluate is the data for this use case.
type Evaluate struct {
	rtConfig    config.Config
	ucGetZettel *GetZettel
	ucQuery     *Query
63
64
65
66
67
68
69







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







+
+
+
+
+
+
+










	if bn == nil {
		return nil
	}
	bns := ast.BlockSlice{bn}
	evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns)
	return bns
}

// RunMetadata executes the use case for a metadata value.
func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice {
	is := parser.ParseMetadata(value)
	evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
	return is
}

// GetZettel retrieves the full zettel of a given zettel identifier.
func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	return uc.ucGetZettel.Run(ctx, zid)
}

// QueryMeta returns a list of metadata that comply to the given selection criteria.
func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	return uc.ucQuery.Run(ctx, q)
}

Changes to usecase/get_all_zettel.go.

12
13
14
15
16
17
18
19
20


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


19
20
21
22
23
24
25
26
27







-
-
+
+







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

package usecase

import (
	"context"

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

// GetAllZettelPort is the interface used by this use case.
type GetAllZettelPort interface {
	GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error)
}

Changes to usecase/get_special_zettel.go.

13
14
15
16
17
18
19
20
21
22
23




24
25
26
27
28
29
30
13
14
15
16
17
18
19




20
21
22
23
24
25
26
27
28
29
30







-
-
-
-
+
+
+
+








package usecase

import (
	"context"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"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.
41
42
43
44
45
46
47
48
49


50
51
52


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

68
69

70
71
72
73
74
75
76
41
42
43
44
45
46
47


48
49
50


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

67
68

69
70
71
72
73
74
75
76







-
-
+
+

-
-
+
+














-
+

-
+








// 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 meta.Value) (zettel.Zettel, error) {
	tag = tag.NormalizeTag()
func (uc TagZettel) Run(ctx context.Context, tag string) (zettel.Zettel, error) {
	tag = meta.NormalizeTag(tag)
	q := query.Parse(
		meta.KeyTitle + api.SearchOperatorEqual + string(tag) + " " +
			meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleTag)
		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 meta.Value }
type ErrTagZettelNotFound struct{ Tag string }

func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + string(etznf.Tag) }
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 {
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
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









-
+

-
-
+
+














-
+

-
+
-
-

// 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 meta.Value) (zettel.Zettel, error) {
func (uc RoleZettel) Run(ctx context.Context, role string) (zettel.Zettel, error) {
	q := query.Parse(
		meta.KeyTitle + api.SearchOperatorEqual + string(role) + " " +
			meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleRole)
		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 meta.Value }
type ErrRoleZettelNotFound struct{ Role string }

func (etznf ErrRoleZettelNotFound) Error() string {
func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + etznf.Role }
	return "role zettel not found: " + string(etznf.Role)
}

Changes to usecase/get_user.go.

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


26
27
28
29
30
31
32
13
14
15
16
17
18
19


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







-
-




+
+








package usecase

import (
	"context"

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

// Use case: return user identified by meta key ident.
// ---------------------------------------------------

// GetUserPort is the interface used by this use case.
type GetUserPort interface {
49
50
51
52
53
54
55
56

57
58
59
60

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

56
57
58
59

60
61
62
63
64
65
66
67







-
+



-
+







func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) {
	ctx = box.NoEnrichContext(ctx)

	// It is important to try first with the owner. First, because another user
	// could give herself the same ''ident''. Second, in most cases the owner
	// will authenticate.
	identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner())
	if err == nil && string(identZettel.Meta.GetDefault(meta.KeyUserID, "")) == ident {
	if err == nil && identZettel.Meta.GetDefault(api.KeyUserID, "") == ident {
		return identZettel.Meta, nil
	}
	// Owner was not found or has another ident. Try via list search.
	q := query.Parse(meta.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident)
	q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident)
	metaList, err := uc.port.SelectMeta(ctx, nil, q)
	if err != nil {
		return nil, err
	}
	if len(metaList) < 1 {
		return nil, nil
	}
90
91
92
93
94
95
96
97

98
99
100
101
90
91
92
93
94
95
96

97
98
99
100
101







-
+




func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) {
	userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid)
	if err != nil {
		return nil, err
	}

	userMeta := userZettel.Meta
	if val, ok := userMeta.Get(meta.KeyUserID); !ok || string(val) != ident {
	if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident {
		return nil, nil
	}
	return userMeta, nil
}

Changes to usecase/get_zettel.go.

12
13
14
15
16
17
18
19
20


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


19
20
21
22
23
24
25
26
27







-
-
+
+







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

package usecase

import (
	"context"

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

// GetZettelPort is the interface used by this use case.
type GetZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

Changes to usecase/lists.go.

13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30







-



+








package usecase

import (
	"context"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// -------- List syntax ------------------------------------------------------

// ListSyntaxPort is the interface used by this use case.
type ListSyntaxPort interface {
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
38
39
40
41
42
43
44
45

46
47
48
49
50

51
52
53
54
55
56
57
38
39
40
41
42
43
44

45
46
47
48
49

50
51
52
53
54
55
56
57







-
+




-
+







// NewListSyntax creates a new use case.
func NewListSyntax(port ListSyntaxPort) ListSyntax {
	return ListSyntax{port: port}
}

// Run executes the use case.
func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) {
	q := query.Parse(meta.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key
	q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
	if err != nil {
		return nil, err
	}
	result := meta.CreateArrangement(metas, meta.KeySyntax)
	result := meta.CreateArrangement(metas, api.KeySyntax)
	for _, syn := range parser.GetSyntaxes() {
		if _, found := result[syn]; !found {
			delete(result, syn)
		}
	}
	return result, nil
}
71
72
73
74
75
76
77
78

79
80
81
82
83

84
71
72
73
74
75
76
77

78
79
80
81
82

83
84







-
+




-
+

// NewListRoles creates a new use case.
func NewListRoles(port ListRolesPort) ListRoles {
	return ListRoles{port: port}
}

// Run executes the use case.
func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) {
	q := query.Parse(meta.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key
	q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
	if err != nil {
		return nil, err
	}
	return meta.CreateArrangement(metas, meta.KeyRole), nil
	return meta.CreateArrangement(metas, api.KeyRole), nil
}

Changes to usecase/parse_zettel.go.

12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29







-



+







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

package usecase

import (
	"context"

	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
)

// ParseZettel is the data for this use case.
type ParseZettel struct {
	rtConfig  config.Config
	getZettel GetZettel
}

Changes to usecase/query.go.

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31


32
33
34
35
36
37
38
15
16
17
18
19
20
21

22


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







-
+
-
-







+
+








import (
	"context"
	"errors"
	"fmt"
	"strings"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// QueryPort is the interface used by this use case.
type QueryPort interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
116
117
118
119
120
121
122
123

124
125
126
127

128
129
130
131
132
133
134
135
136
137
138
139
116
117
118
119
120
121
122

123
124
125
126

127





128
129
130
131
132
133
134







-
+



-
+
-
-
-
-
-







func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta {
	return spec.Execute(ctx, metaSeq, uc.port)
}

func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
		if err != nil {
			continue
		}
		for _, ln := range collect.Order(zn) {
		for _, ref := range collect.Order(zn) {
			ref := ln.Ref
			if !ref.IsZettel() {
				continue
			}

			if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
				if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {
					result = append(result, z.Meta)
				}
			}
		}
	}
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
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







-
-
+
+



-
-
+
+

-
+



-
-
+
+










-
+











+











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







		sb.WriteString(word)
	}
	q := (*query.Query)(nil).Parse(sb.String())
	candidates, err := uc.port.SelectMeta(ctx, nil, q)
	if err != nil {
		return nil
	}
	metaZids := idset.NewCap(len(metaSeq))
	refZids := idset.NewCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel
	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 key, val := range m.ComputedRest() {
			switch meta.Type(key) {
		for _, pair := range m.ComputedPairsRest() {
			switch meta.Type(pair.Key) {
			case meta.TypeID:
				if zid, errParse := id.Parse(string(val)); errParse == nil {
				if zid, errParse := id.Parse(pair.Value); errParse == nil {
					refZids.Add(zid)
				}
			case meta.TypeIDSet:
				for val := range val.Fields() {
					if zid, errParse := id.Parse(val); errParse == nil {
				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 *idset.Set) []*meta.Meta {
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 {
	result := make([]*meta.Meta, 0, len(candidates))
candLoop:
	for _, cand := range candidates {
		zettel, err := uc.port.GetZettel(ctx, cand.Zid)
		if err != nil {
			continue
		}
		v := unlinkedVisitor{
			words: words,
			found: false,
		}
		v.text = v.joinWords(words)

		for _, pair := range zettel.Meta.Pairs() {
			if meta.Type(pair.Key) != meta.TypeZettelmarkup {
				continue
			}
			is := uc.ucEvaluate.RunMetadata(ctx, pair.Value)
			ast.Walk(&v, &is)
			if v.found {
				result = append(result, cand)
				continue candLoop
			}
		}

		syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))
		syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax)
		if !parser.IsASTParser(syntax) {
			continue
		}
		zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax)
		ast.Walk(&v, &zn.Ast)
		if v.found {
			result = append(result, cand)

Changes to usecase/reindex.go.

12
13
14
15
16
17
18
19
20


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


19
20
21
22
23
24
25
26
27







-
-
+
+







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

package usecase

import (
	"context"

	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/logger"
	"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
}

Changes to usecase/update_zettel.go.

12
13
14
15
16
17
18
19

20
21
22
23


24
25
26
27
28
29
30
12
13
14
15
16
17
18

19

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







-
+
-



+
+







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

package usecase

import (
	"context"

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

// UpdateZettelPort is the interface used by this use case.
type UpdateZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)

51
52
53
54
55
56
57
58
59
60



61
62
63

64
65
66
67


68
69
70
71
72
73
74
75
76
77
52
53
54
55
56
57
58



59
60
61
62
63

64
65
66


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







-
-
-
+
+
+


-
+


-
-
+
+










		return err
	}
	if zettel.Equal(oldZettel, false) {
		return nil
	}

	// Update relevant computed, but stored values.
	if _, found := m.Get(meta.KeyCreated); !found {
		if val, crFound := oldZettel.Meta.Get(meta.KeyCreated); crFound {
			m.Set(meta.KeyCreated, val)
	if _, found := m.Get(api.KeyCreated); !found {
		if val, crFound := oldZettel.Meta.Get(api.KeyCreated); crFound {
			m.Set(api.KeyCreated, val)
		}
	}
	m.SetNow(meta.KeyModified)
	m.SetNow(api.KeyModified)

	m.YamlSep = oldZettel.Meta.YamlSep
	if m.Zid == id.ZidConfiguration {
		m.Set(meta.KeySyntax, meta.ValueSyntaxNone)
	if m.Zid == id.ConfigurationZid {
		m.Set(api.KeySyntax, meta.SyntaxNone)
	}

	if !hasContent {
		zettel.Content = oldZettel.Content
	}
	zettel.Content.TrimSpace()
	err = uc.port.UpdateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel")
	return err
}

Changes to web/adapter/adapter.go.

14
15
16
17
18
19
20
21
22


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


21
22
23
24
25
26
27
28
29







-
-
+
+







// Package adapter provides handlers for web requests, and some helper tools.
package adapter

import (
	"context"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/meta"
)

// TryReIndex executes a re-index if the appropriate query action is given.
func TryReIndex(ctx context.Context, actions []string, metaSeq []*meta.Meta, reIndex *usecase.ReIndex) ([]string, error) {
	if lenActions := len(actions); lenActions > 0 {
		tempActions := make([]string, 0, lenActions)
		hasReIndex := false

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

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

31
32
33
34
35
36
37
17
18
19
20
21
22
23

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







-






+







import (
	"bytes"
	"context"
	"net/http"
	"time"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
)

// API holds all data and methods for delivering API call results.
type API struct {
	log      *logger.Logger
	b        server.Builder
	authz    auth.AuthzManager

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

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

26
27
28
29
30
31
32
14
15
16
17
18
19
20

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







-




+







package api

import (
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
52
53
54
55
56
57
58
59

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

59
60
61
62
63
64
65
66







-
+







		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		var result []byte
		var contentType string
		location := a.NewURLBuilder('z').SetZid(newZid)
		location := a.NewURLBuilder('z').SetZid(newZid.ZettelID())
		switch enc {
		case api.EncoderPlain:
			result = newZid.Bytes()
			contentType = content.PlainText
		case api.EncoderData:
			result = []byte(sx.Int64(newZid).String())
			contentType = content.SXPF

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

12
13
14
15
16
17
18
19
20


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


19
20
21
22
23
24
25
26
27







-
-
+
+







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

package api

import (
	"net/http"

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

// MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {

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

13
14
15
16
17
18
19
20
21


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


20
21
22
23
24
25
26
27
28







-
-
+
+








package api

import (
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
)

// MakeGetDataHandler creates a new HTTP handler to return zettelstore data.
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		version := ucVersion.Run()
		err := a.writeObject(w, id.Invalid, sx.MakeList(

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

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

30
31
32


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


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







-
-




+



+
+







	"bytes"
	"context"
	"fmt"
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings.
func (a *API) MakeGetZettelHandler(
	getZettel usecase.GetZettel,
	parseZettel usecase.ParseZettel,
	evaluate usecase.Evaluate,
53
54
55
56
57
58
59

60
61


62
63




64
65
66
67
68
69

70
71
72
73
74
75
76
54
55
56
57
58
59
60
61
62

63
64
65

66
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82







+

-
+
+

-
+
+
+
+





-
+







			a.writePlainData(ctx, w, zid, part, getZettel)

		case api.EncoderData:
			a.writeSzData(ctx, w, zid, part, getZettel)

		default:
			var zn *ast.ZettelNode
			var em func(value string) ast.InlineSlice
			if q.Has(api.QueryKeyParseOnly) {
				zn, err = parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax))
				zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
				em = parser.ParseMetadata
			} else {
				zn, err = evaluate.Run(ctx, zid, q.Get(meta.KeySyntax))
				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, enc, encStr, part)
			a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part)
		}
	})
}

func (a *API) writePlainData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var contentType string
93
94
95
96
97
98
99
100

101
102
103
104
105
106
107
99
100
101
102
103
104
105

106
107
108
109
110
111
112
113







-
+







		}

	case partMeta:
		contentType = content.PlainText
		_, err = z.Meta.Write(&buf)

	case partContent:
		contentType = content.MIMEFromSyntax(string(z.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax))
		_, err = z.Content.Write(&buf)
	}

	if err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
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
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







+





-
+









-
+

-
+







		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, meta.KeyLang),
			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 {
	case partZettel:
		_, err = encdr.WriteZettel(&buf, zn)
		_, err = encdr.WriteZettel(&buf, zn, evalMeta)
	case partMeta:
		_, err = encdr.WriteMeta(&buf, zn.InhMeta)
		_, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta)
	case partContent:
		_, err = encdr.WriteContent(&buf, zn)
	}
	if err != nil {
		a.log.Error().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return

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

14
15
16
17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31







-



+







package api

import (
	"net/http"
	"time"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {

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

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


34
35
36
37
38
39
40
20
21
22
23
24
25
26


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







-
-





+
+







	"net/http"
	"net/url"
	"strconv"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,
60
61
62
63
64
65
66
67

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

67
68
69
70
71
72
73
74







-
+







			return
		}
		if len(actions) > 0 {
			if len(metaSeq) > 0 {
				for _, act := range actions {
					if act == api.RedirectAction {
						zid := metaSeq[0].Zid
						ub := a.NewURLBuilder('z').SetZid(zid)
						ub := a.NewURLBuilder('z').SetZid(zid.ZettelID())
						a.redirectFound(w, r, ub, zid)
						return
					}
				}
			}
		}

171
172
173
174
175
176
177
178

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

178
179
180
181
182
183
184
185







-
+







			return err
		}
	}
	return nil
}
func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error {
	for key, ml := range arr {
		_, err := io.WriteString(w, string(key))
		_, err := io.WriteString(w, key)
		if err != nil {
			return err
		}
		for i, m := range ml {
			if i == 0 {
				_, err = io.WriteString(w, "\t")
			} else {
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
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







-
+
-

-
+
-
-
-
+
+

+
-
+






-
+










-
+





-
+


















-
+





-
+







		sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
		sx.MakeList(result...),
	))
	return err
}
func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
	var lb sx.ListBuilder
	result := sx.Nil()
	lb.Add(sx.SymbolList)
	for aggKey, metaList := range arr {
		var lbMeta sx.ListBuilder
		sxMeta := sx.Nil()
		lbMeta.Add(sx.MakeString(string(aggKey)))
		for _, m := range metaList {
			lbMeta.Add(sx.Int64(m.Zid))
		for i := len(metaList) - 1; i >= 0; i-- {
			sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
		}
		sxMeta = sxMeta.Cons(sx.MakeString(aggKey))
		lb.Add(lbMeta.List())
		result = result.Cons(sxMeta)
	}
	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("aggregate"),
		sx.MakeString(act),
		sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
		lb.List(),
		result.Cons(sx.SymbolList),
	))
	return err
}

func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)
	if tag == "" {
		return false
	}
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, meta.Value(tag))
	z, err := tagZettel.Run(ctx, tag)
	if err != nil {
		a.reportUsecaseError(w, err)
		return true
	}
	zid := z.Meta.Zid
	newURL := a.NewURLBuilder('z').SetZid(zid)
	newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID())
	for key, slVals := range vals {
		if key == api.QueryKeyTag {
			continue
		}
		for _, val := range slVals {
			newURL.AppendKVQuery(key, val)
		}
	}
	a.redirectFound(w, r, newURL, zid)
	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, meta.Value(role))
	z, err := roleZettel.Run(ctx, role)
	if err != nil {
		a.reportUsecaseError(w, err)
		return true
	}
	zid := z.Meta.Zid
	newURL := a.NewURLBuilder('z').SetZid(zid)
	newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID())
	for key, slVals := range vals {
		if key == api.QueryKeyRole {
			continue
		}
		for _, val := range slVals {
			newURL.AppendKVQuery(key, val)
		}

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

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


28
29
30
31
32
33
34
16
17
18
19
20
21
22


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







-
-



+
+







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

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// getEncoding returns the data encoding selected by the caller.
func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) {
	encoding := q.Get(api.QueryKeyEncoding)
	if encoding != "" {
		return api.Encoder(encoding), encoding
129
130
131
132
133
134
135
136

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

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







-
+













	if err != nil {
		return zettel.Zettel{}, err
	}

	m := meta.New(zid)
	for k, v := range zd.Meta {
		if !meta.IsComputed(k) {
			m.Set(meta.RemoveNonGraphic(k), meta.Value(meta.RemoveNonGraphic(v)))
			m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
		}
	}

	var content zettel.Content
	if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil {
		return zettel.Zettel{}, err
	}

	return zettel.Zettel{
		Meta:    m,
		Content: content,
	}, nil
}

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

14
15
16
17
18
19
20
21
22


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


21
22
23
24
25
26
27
28
29







-
-
+
+







package api

import (
	"bytes"
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
)

func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, obj); err != nil {
		msg := a.log.Error().Err(err)
		if msg != nil {

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

13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30







-



+








package api

import (
	"net/http"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {

Changes to web/adapter/response.go.

68
69
70
71
72
73
74
75

76
77
78
79

80
81
82
83
84
85
86
68
69
70
71
72
73
74

75
76
77
78

79
80
81
82
83
84
85
86







-
+



-
+







	}
	var eiz box.ErrInvalidZid
	if errors.As(err, &eiz) {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid)
	}
	var etznf usecase.ErrTagZettelNotFound
	if errors.As(err, &etznf) {
		return http.StatusNotFound, "Tag zettel not found: " + string(etznf.Tag)
		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: " + string(erznf.Role)
		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)

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

16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32


33
34
35
36
37
38
39
16
17
18
19
20
21
22

23

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







-
+
-








+
+







import (
	"bytes"
	"context"
	"net/http"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/box"
	"zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel,
	createZettel *usecase.CreateZettel,
60
61
62
63
64
65
66
67

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

68
69
70
71
72
73
74
75







-
+







		switch op {
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := parser.NormalizedSpacedText(q.Get(meta.KeyTitle))
			newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle))
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)
		case actionSequel:
			wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		}
	})
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
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







-
-
+
+

-
+


-
+










-
+







	roleData []string,
	syntaxData []string,
) {
	user := server.GetUser(ctx)
	m := ztl.Meta

	var sb strings.Builder
	for key, val := range m.Rest() {
		sb.WriteString(key)
	for _, p := range m.PairsRest() {
		sb.WriteString(p.Key)
		sb.WriteString(": ")
		sb.WriteString(string(val))
		sb.WriteString(p.Value)
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnv(ctx, "form", wui.getUserLang(ctx), title, user)
	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.ZidFormTemplate, env)
		rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}
}

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
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
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







-
+

-
+




















-
+










-
-
+
+

-
+







		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid))
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid))
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
		}
	})
}

// MakeGetZettelFromListHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeGetZettelFromListHandler(
	queryMeta *usecase.Query,
	evaluate *usecase.Evaluate,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		q := adapter.GetQuery(r.URL.Query())
		ctx := r.Context()
		metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq)
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig)
		bns := evaluate.RunBlockNode(ctx, entries)
		enc := zmkenc.Create()
		var zmkContent bytes.Buffer
		_, err = enc.WriteBlocks(&zmkContent, &bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		m := meta.New(id.Invalid)
		m.Set(meta.KeyTitle, meta.Value(q.Human()))
		m.Set(meta.KeySyntax, meta.ValueSyntaxZmk)
		m.Set(api.KeyTitle, q.Human())
		m.Set(api.KeySyntax, api.ValueSyntaxZmk)
		if qval := q.String(); qval != "" {
			m.Set(meta.KeyQuery, meta.Value(qval))
			m.Set(api.KeyQuery, qval)
		}
		zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())}
		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData)
	})
}

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

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




25
26


27
28
29
30
31
32
33
11
12
13
14
15
16
17

18
19




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







-


-
-
-
-
+
+
+
+


+
+







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

package webui

import (
	"net/http"
	"slices"

	"t73f.de/r/sx"
	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/box"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/box"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(
	getZettel usecase.GetZettel,
	getAllZettel usecase.GetAllZettel,
46
47
48
49
50
51
52

53

54
55

56
57
58
59
60
61
62
63
64

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


77
78
79
80
81
82
83
84
85
86

87
88
89
90
91
92

93
94
95
96
97





98
99
100
101
102
103
104
47
48
49
50
51
52
53
54

55
56

57
58
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73
74
75
76


77
78
79
80
81
82
83
84
85
86
87

88
89
90
91
92
93

94
95
96



97
98
99
100
101
102
103
104
105
106
107
108







+
-
+

-
+








-
+










-
-
+
+









-
+





-
+


-
-
-
+
+
+
+
+







			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "delete",
			ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user)
			wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.MakeString(string(zs[1].Meta.GetDefault(meta.KeyBoxNumber, "???"))))
			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.ZidDeleteTemplate, env)
			err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	})
}

func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair {
	zidMap := set.New[string]()
	addListValues(zidMap, m, meta.KeyBackward)
	zidMap := make(strfun.Set)
	addListValues(zidMap, m, api.KeyBackward)
	for _, kd := range meta.GetSortedKeyDescriptions() {
		inverseKey := kd.Inverse
		if inverseKey == "" {
			continue
		}
		ikd := meta.GetDescription(inverseKey)
		switch ikd.Type {
		case meta.TypeID:
			if val, ok := m.Get(inverseKey); ok {
				zidMap.Add(string(val))
				zidMap.Set(val)
			}
		case meta.TypeIDSet:
			addListValues(zidMap, m, inverseKey)
		}
	}
	return wui.zidLinksSxn(slices.Sorted(zidMap.Values()), getTextTitle)
	return wui.zidLinksSxn(maps.Keys(zidMap), getTextTitle)
}

func addListValues(zidMap *set.Set[string], m *meta.Meta, key string) {
	for val := range m.GetFields(key) {
		zidMap.Add(val)
func addListValues(zidMap strfun.Set, m *meta.Meta, key string) {
	if values, ok := m.GetList(key); ok {
		for _, val := range values {
			zidMap.Set(val)
		}
	}
}

// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

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

12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29







-



+







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

package webui

import (
	"net/http"

	"t73f.de/r/zsc/domain/id"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(
	getZettel usecase.GetZettel,
	ucListRoles usecase.ListRoles,
74
75
76
77
78
79
80
81

82
83

84
85
86
74
75
76
77
78
79
80

81
82

83
84
85
86







-
+

-
+



		}
		if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid))
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid))
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID()))
		}
	})
}

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

18
19
20
21
22
23
24
25

26
27
28
29
30
31


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

25

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







-
+
-





+
+







	"errors"
	"io"
	"net/http"
	"regexp"
	"strings"
	"unicode"

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

var (
	bsCRLF = []byte{'\r', '\n'}
	bsLF   = []byte{'\n'}
)

50
51
52
53
54
55
56
57

58
59
60

61
62

63
64

65
66
67
68

69
70
71

72
73
74
75
76
77
78
51
52
53
54
55
56
57

58
59
60

61
62

63
64

65
66
67
68

69
70
71

72
73
74
75
76
77
78
79







-
+


-
+

-
+

-
+



-
+


-
+







	if postMeta, ok := trimmedFormValue(r, "meta"); ok {
		m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta))))
		m.Sanitize()
	} else {
		m = meta.New(zid)
	}
	if postTitle, ok := trimmedFormValue(r, "title"); ok {
		m.Set(meta.KeyTitle, meta.Value(meta.RemoveNonGraphic(postTitle)))
		m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
	}
	if postTags, ok := trimmedFormValue(r, "tags"); ok {
		if tags := meta.Value(meta.RemoveNonGraphic(postTags)).AsSlice(); len(tags) > 0 {
		if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
			for i, tag := range tags {
				tags[i] = string(meta.Value(tag).NormalizeTag())
				tags[i] = meta.NormalizeTag(tag)
			}
			m.SetList(meta.KeyTags, tags)
			m.SetList(api.KeyTags, tags)
		}
	}
	if postRole, ok := trimmedFormValue(r, "role"); ok {
		m.SetWord(meta.KeyRole, meta.RemoveNonGraphic(postRole))
		m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole))
	}
	if postSyntax, ok := trimmedFormValue(r, "syntax"); ok {
		m.SetWord(meta.KeySyntax, meta.RemoveNonGraphic(postSyntax))
		m.SetWord(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
	}

	if data := textContent(r); data != nil {
		return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil
	}
	if data, m2 := uploadedContent(r, m); data != nil {
		return doSave, zettel.Zettel{Meta: m2, Content: zettel.NewContent(data)}, nil
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
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







-
+









-
-
+
+


-
+












			if err2 != nil {
				return nil, m
			}
			if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 {
				ct := cts[0]
				if fileSyntax := content.SyntaxFromMIME(ct, data); fileSyntax != "" {
					m = m.Clone()
					m.Set(meta.KeySyntax, meta.Value(fileSyntax))
					m.Set(api.KeySyntax, fileSyntax)
				}
			}
			return data, m
		}
	}
	return nil, m
}

func allowEmptyContent(m *meta.Meta) bool {
	if syntax, found := m.Get(meta.KeySyntax); found {
		if syntax == meta.ValueSyntaxNone {
	if syntax, found := m.Get(api.KeySyntax); found {
		if syntax == api.ValueSyntaxNone {
			return true
		}
		if pinfo := parser.Get(string(syntax)); pinfo != nil {
		if pinfo := parser.Get(syntax); pinfo != nil {
			return pinfo.IsTextFormat
		}
	}
	return true
}

var reEmptyLines = regexp.MustCompile(`(\n|\r)+\s*(\n|\r)+`)

func removeEmptyLines(s []byte) []byte {
	b := bytes.TrimSpace(s)
	return reEmptyLines.ReplaceAllLiteral(b, []byte{'\n'})
}

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

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

36
37
38
39
40
41
42
17
18
19
20
21
22
23


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







-
-