Zettelstore

Check-in Differences
Login

Check-in Differences

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

Difference From v0.21.0 To trunk

2025-05-09
16:34
Add loopback device authentication ... (Leaf check-in: 90dbaa8009 user: stern tags: trunk)
12:43
Omit rporting frozen config error ... (check-in: 336d20b3e4 user: stern tags: trunk)
2025-04-26
15:13
WebUI: move context link from info page to zettel page ... (check-in: 7ae5e31c4a user: stern tags: trunk)
2025-04-17
15:29
Version 0.21.0 ... (check-in: 7220c2d479 user: stern tags: trunk, release, v0.21.0)
09:24
Update to zsc, to fix a panic ... (check-in: b3283fc6d6 user: stern tags: trunk)

Changes to VERSION.
1


1
-
+
0.21.0
0.22.0-dev
Changes to cmd/main.go.
167
168
169
170
171
172
173


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







+
+







	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyInsecureHTML      = "insecure-html"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyLoopbackIdent     = "loopback-ident"
	keyLoopbackZid       = "loopback-zid"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyReadOnly          = "read-only-mode"
	keyRuntimeProfiling  = "runtime-profiling"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
213
214
215
216
217
218
219


220
221
222
223
224
225
226
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230







+
+







	}

	err = setConfigValue(
		err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))

	err = setConfigValue(
		err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	err = setConfigValue(err, kernel.WebService, kernel.WebLoopbackIdent, cfg.GetDefault(keyLoopbackIdent, ""))
	err = setConfigValue(err, kernel.WebService, kernel.WebLoopbackZid, cfg.GetDefault(keyLoopbackZid, ""))
	if val, found := cfg.Get(keyBaseURL); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val)
	}
	if val, found := cfg.Get(keyURLPrefix); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val)
	}
	err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
293
294
295
296
297
298
299
300




301
302
303
304
305
306
307
297
298
299
300
301
302
303

304
305
306
307
308
309
310
311
312
313
314







-
+
+
+
+







		func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error {
			setupRouting(srv, plMgr, authMgr, rtConfig)
			return nil
		},
	)

	if command.Simple {
		kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true")
		if err := kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true"); err != nil {
			kern.GetKernelLogger().Error().Err(err).Msg("unable to set simple-mode")
			return 1
		}
	}
	kern.Start(command.Header, command.LineServer, filename)
	exitCode, err := command.Func(fs)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
	}
	kern.Shutdown(true)
331
332
333
334
335
336
337

338
339

340
341

342





343




344
345
346
347
348
349
350
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







+

-
+

-
+

+
+
+
+
+
-
+
+
+
+







	fullVersion := info.revision
	if info.dirty {
		fullVersion += "-dirty"
	}
	kernel.Main.Setup(progName, fullVersion, info.time)
	flag.Parse()
	if *cpuprofile != "" || *memprofile != "" {
		var err error
		if *cpuprofile != "" {
			kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile)
			err = kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile)
		} else {
			kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile)
			err = kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile)
		}
		if err != nil {
			kernel.Main.GetKernelLogger().Error().Err(err).Msg("start profiling")
			return 1
		}
		defer func() {
		defer kernel.Main.StopProfiling()
			if err = kernel.Main.StopProfiling(); err != nil {
				kernel.Main.GetKernelLogger().Error().Err(err).Msg("stop profiling")
			}
		}()
	}
	args := flag.Args()
	if len(args) == 0 {
		return runSimple()
	}
	return executeCommand(args[0], args[1:]...)
}
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: 20250509182657

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.

91
92
93
94
95
96
97












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







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








  Default: ""info"".

  Examples: ""error"" will produce just error messages (e.g. no ""info"" messages).
  ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components.

  When you are familiar with operating the Zettelstore, you might set the level to ""error"" to receive fewer noisy messages from it.
; [!loopback-ident|''loopback-ident''], [!loopback-zid|''loopback-zid'']
: These keys are effective only if [[authentication is enabled|00001010000000]].
  They must specify the user ID and zettel ID of a [[user zettel|00001010040200]].
  When these keys are set and an HTTP request originates from the loopback device, no further authentication is required.

  The loopback device typically uses the IP address ''127.0.0.1'' (IPv4) or ''::1'' (IPv6).

  This configuration allows client software running on the same computer as the Zettelstore to access it through its API or web user interface.

  However, this setup is not recommended if the Zettelstore is running on a computer shared with untrusted or unknown users.

  Default: (empty string)/00000000000000
; [!max-request-size|''max-request-size'']
: It limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources.
  The minimum value is 1024.

  Default: 16777216 (16 MiB). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.
Changes to docs/manual/00001010040100.zettel.
1
2
3
4
5
6
7


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

7
8
9
10
11
12
13
14






-
+
+






id: 00001010040100
title: Enable authentication
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220419192817
modified: 20250509181249
show-back-links: close

To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner.
Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''.
Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled.

Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to some random string data (minimum length is 16 bytes) to secure the data exchanged with a client system.
Changes to go.mod.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15









16
17
18
19

1
2
3
4
5
6









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

19






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



-
+
module zettelstore.de/z

go 1.24

require (
	github.com/fsnotify/fsnotify v1.9.0
	github.com/yuin/goldmark v1.7.9
	golang.org/x/crypto v0.37.0
	golang.org/x/term v0.31.0
	golang.org/x/text v0.24.0
	t73f.de/r/sx v0.0.0-20250415161954-42ed9c4d6abc
	t73f.de/r/sxwebs v0.0.0-20250415162443-110f49c5a1ae
	t73f.de/r/webs v0.0.0-20250403120312-69ac186a05b5
	t73f.de/r/zero v0.0.0-20250403120114-7d464a82f2e5
	t73f.de/r/zsc v0.0.0-20250417145802-ef331b4f0cdd
	github.com/yuin/goldmark v1.7.11
	golang.org/x/crypto v0.38.0
	golang.org/x/term v0.32.0
	golang.org/x/text v0.25.0
	t73f.de/r/sx v0.0.0-20250506141526-34e7a17d22f1
	t73f.de/r/sxwebs v0.0.0-20250505103220-54507dc1509d
	t73f.de/r/webs v0.0.0-20250505155652-db844dadbf0a
	t73f.de/r/zero v0.0.0-20250421161051-9472f592aeea
	t73f.de/r/zsc v0.0.0-20250502143402-b5c74e61e1a7
	t73f.de/r/zsx v0.0.0-20250415162540-fc13b286b6ce
)

require golang.org/x/sys v0.32.0 // indirect
require golang.org/x/sys v0.33.0 // 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




















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


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


github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/yuin/goldmark v1.7.9 h1:rVSeT+f7/lAM+bJHVm5YHGwNrnd40i1Ch2DEocEjHQ0=
github.com/yuin/goldmark v1.7.9/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
t73f.de/r/sx v0.0.0-20250415161954-42ed9c4d6abc h1:tlsP+47Rf8i9Zv1TqRnwfbQx3nN/F/92RkT6iCA6SVA=
t73f.de/r/sx v0.0.0-20250415161954-42ed9c4d6abc/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg=
t73f.de/r/sxwebs v0.0.0-20250415162443-110f49c5a1ae h1:K6nxN/bb0BCSiDffwNPGTF2uf5WcTdxcQXzByXNuJ7M=
t73f.de/r/sxwebs v0.0.0-20250415162443-110f49c5a1ae/go.mod h1:0LQ9T1svSg9ADY/6vQLKNUu6LqpPi8FGr7fd2qDT5H8=
t73f.de/r/webs v0.0.0-20250403120312-69ac186a05b5 h1:M2RMwkuBlMAKgNH70qLtEFqw7jtH+/rM0NTUPZObk6Y=
t73f.de/r/webs v0.0.0-20250403120312-69ac186a05b5/go.mod h1:zk92hSKB4iWyT290+163seNzu350TA9XLATC9kOldqo=
t73f.de/r/zero v0.0.0-20250403120114-7d464a82f2e5 h1:3FU6YUaqxJI20ZTXDc8xnPBzjajnWZ56XaPOfckEmJo=
t73f.de/r/zero v0.0.0-20250403120114-7d464a82f2e5/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA=
t73f.de/r/zsc v0.0.0-20250417145802-ef331b4f0cdd h1:InJjWI/Y2xAJBBs+SMXTS7rJaizk6/b/VW5CCno0vrg=
t73f.de/r/zsc v0.0.0-20250417145802-ef331b4f0cdd/go.mod h1:Mb3fLaOcSp5t6I7sRDKmOx4U1AnTb30F7Jh4e1L0mx8=
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
t73f.de/r/sx v0.0.0-20250506141526-34e7a17d22f1 h1:XcJyCHhfdA8USdVPgSl2QB/xodif22t8U5Tnb1H9MWk=
t73f.de/r/sx v0.0.0-20250506141526-34e7a17d22f1/go.mod h1:1RlIqF7OJqEv7j5uhY+gNWkBYEYgVTpAUbGG20rK31Y=
t73f.de/r/sxwebs v0.0.0-20250505103220-54507dc1509d h1:Byow/3dJIeBONdF0XrdSmpzfWcGdCmIwQRU4D4DbKrQ=
t73f.de/r/sxwebs v0.0.0-20250505103220-54507dc1509d/go.mod h1:+uH4dTQ+I8eofO+rJX37yn1LSLf6V7/7lpRwUpwtCpE=
t73f.de/r/webs v0.0.0-20250505155652-db844dadbf0a h1:ohQthZH4IsyD0YuPurawY89pPUm2sFNrQwKUSRUr9So=
t73f.de/r/webs v0.0.0-20250505155652-db844dadbf0a/go.mod h1:zk92hSKB4iWyT290+163seNzu350TA9XLATC9kOldqo=
t73f.de/r/zero v0.0.0-20250421161051-9472f592aeea h1:7oOWX1J1YC9gadK/poa4g6upF9l8dQa9uNdqYFEmKm8=
t73f.de/r/zero v0.0.0-20250421161051-9472f592aeea/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA=
t73f.de/r/zsc v0.0.0-20250502143402-b5c74e61e1a7 h1:fn40OYrzWt8KUNpB6b77dej07H8sDA6WaqcFT4ifSW4=
t73f.de/r/zsc v0.0.0-20250502143402-b5c74e61e1a7/go.mod h1:Mb3fLaOcSp5t6I7sRDKmOx4U1AnTb30F7Jh4e1L0mx8=
t73f.de/r/zsx v0.0.0-20250415162540-fc13b286b6ce h1:R9rtg4ecx4YYixsMmsh+wdcqLdY9GxoC5HZ9mMS33to=
t73f.de/r/zsx v0.0.0-20250415162540-fc13b286b6ce/go.mod h1:tXOlmsQBoY4mY7Plu0LCCMZNSJZJbng98fFarZXAWvM=
Changes to internal/auth/impl/impl.go.
55
56
57
58
59
60
61
62

63
64
65

66
67
68
69
70
71
72
55
56
57
58
59
60
61

62
63
64

65
66
67
68
69
70
71
72







-
+


-
+







	kernel.CoreGoArch,
	kernel.CoreVersion,
}

func calcSecret(extSecret string) []byte {
	h := fnv.New128()
	if extSecret != "" {
		io.WriteString(h, extSecret)
		_, _ = io.WriteString(h, extSecret)
	}
	for _, key := range configKeys {
		io.WriteString(h, kernel.Main.GetConfig(kernel.CoreService, key).(string))
		_, _ = io.WriteString(h, kernel.Main.GetConfig(kernel.CoreService, key).(string))
	}
	return h.Sum(nil)
}

// IsReadonly returns true, if the systems is configured to run in read-only-mode.
func (a *myAuth) IsReadonly() bool { return a.readonly }

Changes to internal/box/constbox/constbox.go.
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
256
257
258
259
260
261
262

263
264











265
266
267
268
269
270
271







-


-
-
-
-
-
-
-
-
-
-
-







			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(),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.ZidSxnPrelude: {
		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,
		},
		zettel.NewContent(contentPreludeSxn)},
	id.ZidBaseCSS: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxCSS,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240827143500",
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
454
455
456
457
458
459
460



461
462
463
464
465
466
467







-
-
-








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

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

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

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

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

//go:embed menu_lists.zettel
Changes to internal/box/constbox/info.sxn.
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: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " · ") (a (@ (href ,context-url)) "Context")
      (@H " / ") (a (@ (href ,context-full-url)) "Full")
      ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit")))
      (@H " · ") (a (@ (href ,context-full-url)) "Full Context")
      ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit")))
      ,@(ROLE-DEFAULT-actions (current-binding))
      ,@(if (bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex")))
      ,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
Deleted internal/box/constbox/prelude.sxn.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62






























































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

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

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

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

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

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

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


;; or macro
;;
;; (or EXPR ...)
(defmacro or args
    (cond ((null? args)       NIL)
          ((null? (cdr args)) (car args))
          (T                  `(if ,(car args) T (or ,@(cdr args))))))
Changes to internal/box/constbox/zettel.sxn.
19
20
21
22
23
24
25

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







+







      ,zid (@H " · ")
      (a (@ (href ,info-url)) "Info") (@H " · ")
      "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role)))
          ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role))
                `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " · ") ,@tag-refs))
      (@H " · ") (a (@ (href ,context-url)) "Context")
      ,@(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))
      ,@(ROLE-DEFAULT-heading (current-binding))
    )
Changes to internal/box/dirbox/dirbox.go.
310
311
312
313
314
315
316
317
318





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


317
318
319
320
321
322
323
324
325
326
327
328







-
-
+
+
+
+
+







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

Changes to internal/box/filebox/zipbox.go.
66
67
68
69
70
71
72
73



74
75
76
77
78
79
80
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80
81
82







-
+
+
+







}

func (zb *zipBox) Start(context.Context) error {
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return err
	}
	reader.Close()
	if err = reader.Close(); err != nil {
		return err
	}
	zipNotifier := notify.NewSimpleZipNotifier(zb.log, zb.name)
	zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify)
	zb.dirSrv.Start()
	return nil
}

func (zb *zipBox) Refresh(_ context.Context) {
92
93
94
95
96
97
98
99

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

101
102
103
104
105
106
107
108







-
+







	if !entry.IsValid() {
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return zettel.Zettel{}, err
	}
	defer reader.Close()
	defer func() { _ = reader.Close() }()

	var m *meta.Meta
	var src []byte
	var inMeta bool

	contentName := entry.ContentName
	if metaName := entry.MetaName; metaName == "" {
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
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







-













-
+







}

func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return err
	}
	defer reader.Close()
	entries := zb.dirSrv.GetDirEntries(constraint)
	zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta")
	for _, entry := range entries {
		if !constraint(entry.Zid) {
			continue
		}
		m, err2 := zb.readZipMeta(reader, entry.Zid, entry)
		if err2 != nil {
			continue
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
	return reader.Close()
}

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

func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	err := box.ErrReadOnly
	entry := zb.dirSrv.GetDirEntry(zid)
222
223
224
225
226
227
228

229
230





231
223
224
225
226
227
228
229
230


231
232
233
234
235
236







+
-
-
+
+
+
+
+

}

func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) {
	f, err := reader.Open(name)
	if err != nil {
		return nil, err
	}
	data, err := io.ReadAll(f)
	defer f.Close()
	return io.ReadAll(f)
	err2 := f.Close()
	if err == nil {
		err = err2
	}
	return data, err
}
Changes to internal/box/manager/mapstore/mapstore.go.
579
580
581
582
583
584
585
586

587
588
589
590
591
592
593
594
595
596
597

598
599
600
601
602
603
604

605
606
607

608
609
610
611
612
613
614
615
616
617
618

619
620
621
622
623
624
625
626
627
628
629
630
631

632
633
634
635
636
637
638
639


640
641
642
643
644
645

646
647
648


649
650

651
652
653
654
655
656
657
658

659
660

661
662
663
664
665
666
667
668
669

670
671
672


673
674
579
580
581
582
583
584
585

586
587
588
589
590
591
592
593
594
595
596

597
598
599
600
601
602
603

604
605
606

607
608
609
610
611
612
613
614
615
616
617

618
619
620
621
622
623
624
625
626
627
628
629
630

631
632
633
634
635
636
637


638
639
640
641
642
643
644

645
646


647
648
649

650
651
652
653
654
655
656
657

658
659

660
661
662
663
664
665
666
667
668

669
670


671
672
673
674







-
+










-
+






-
+


-
+










-
+












-
+






-
-
+
+





-
+

-
-
+
+

-
+







-
+

-
+








-
+

-
-
+
+


	ms.mxStats.Unlock()
}

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

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

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

		otherRefs := make([]string, 0, len(zi.otherRefs))
		for k := range zi.otherRefs {
			otherRefs = append(otherRefs, k)
		}
		slices.Sort(otherRefs)
		for _, k := range otherRefs {
			fmt.Fprintln(w, "* Meta", k)
			_, _ = fmt.Fprintln(w, "* Meta", k)
			dumpSet(w, "** Forward:", zi.otherRefs[k].forward)
			dumpSet(w, "** Backward:", zi.otherRefs[k].backward)
		}
		dumpStrings(w, "* Words", "", "", zi.words)
		dumpStrings(w, "* URLs", "[[", "]]", zi.urls)
	}
}

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

func dumpSet(w io.Writer, prefix string, s *idset.Set) {
	if !s.IsEmpty() {
		io.WriteString(w, prefix)
		_, _ = io.WriteString(w, prefix)
		s.ForEach(func(zid id.Zid) {
			io.WriteString(w, " ")
			w.Write(zid.Bytes())
			_, _ = io.WriteString(w, " ")
			_, _ = w.Write(zid.Bytes())
		})
		fmt.Fprintln(w)
		_, _ = fmt.Fprintln(w)
	}
}
func dumpStrings(w io.Writer, title, preString, postString string, slice []string) {
	if len(slice) > 0 {
		sl := make([]string, len(slice))
		copy(sl, slice)
		slices.Sort(sl)
		fmt.Fprintln(w, title)
		_, _ = fmt.Fprintln(w, title)
		for _, s := range sl {
			fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString)
			_, _ = fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString)
		}
	}
}

func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) {
	if len(srefs) == 0 {
		return
	}
	fmt.Fprintln(w, "====", title)
	_, _ = fmt.Fprintln(w, "====", title)
	for _, s := range slices.Sorted(maps.Keys(srefs)) {
		fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString)
		fmt.Fprintln(w, ":", srefs[s])
		_, _ = fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString)
		_, _ = fmt.Fprintln(w, ":", srefs[s])
	}
}
Changes to internal/box/notify/fsdir.go.
51
52
53
54
55
56
57
58

59
60
61
62
63
64
65
51
52
53
54
55
56
57

58
59
60
61
62
63
64
65







-
+







	err = watcher.Add(absPath)
	if errParent != nil {
		if err != nil {
			log.Error().
				Str("parentDir", absParentDir).Err(errParent).
				Str("path", absPath).Err(err).
				Msg("Unable to access Zettel directory and its parent directory")
			watcher.Close()
			_ = watcher.Close()
			return nil, err
		}
		log.Info().Str("parentDir", absParentDir).Err(errParent).
			Msg("Parent of Zettel directory cannot be supervised")
		log.Info().Str("path", absPath).
			Msg("Zettelstore might not detect a deletion or movement of the Zettel directory")
	} else if err != nil {
86
87
88
89
90
91
92
93

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

93
94
95
96
97
98
99
100







-
+







}

func (fsdn *fsdirNotifier) Refresh() {
	fsdn.refresh <- struct{}{}
}

func (fsdn *fsdirNotifier) eventLoop() {
	defer fsdn.base.Close()
	defer func() { _ = fsdn.base.Close() }()
	defer close(fsdn.events)
	defer close(fsdn.refresh)
	if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) {
		return
	}

	for fsdn.readAndProcessEvent() {
152
153
154
155
156
157
158
159

160
161
162
163
164
165
166
152
153
154
155
156
157
158

159
160
161
162
163
164
165
166







-
+







	fsdn.log.Trace().Str("path", fsdn.path).Str("name", ev.Name).Str("op", ev.Op.String()).Msg("event does not match")
	return true
}

func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool {
	if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) {
		fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed")
		fsdn.base.Remove(fsdn.path)
		_ = fsdn.base.Remove(fsdn.path)
		select {
		case fsdn.events <- Event{Op: Destroy}:
		case <-fsdn.done:
			fsdn.log.Trace().Int("i", 1).Msg("done dir event processing")
			return false
		}
		return true
Changes to internal/box/notify/helper.go.
53
54
55
56
57
58
59
60
61
62
63
64

65

66
67
68
69
70
71
72
53
54
55
56
57
58
59

60
61
62
63
64

65
66
67
68
69
70
71
72







-




+
-
+







func newZipPathFetcher(zipPath string) EntryFetcher { return &zipPathFetcher{zipPath} }

func (zpf *zipPathFetcher) Fetch() ([]string, error) {
	reader, err := zip.OpenReader(zpf.zipPath)
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	result := make([]string, 0, len(reader.File))
	for _, f := range reader.File {
		result = append(result, f.Name)
	}
	err = reader.Close()
	return result, nil
	return result, err
}

// listDirElements write all files within the directory path as events.
func listDirElements(log *logger.Logger, fetcher EntryFetcher, events chan<- Event, done <-chan struct{}) bool {
	select {
	case events <- Event{Op: Make}:
	case <-done:
Changes to internal/encoder/htmlenc.go.
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80







-
+







	head.AddN(
		shtml.SymHead,
		sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta),
	)
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
		he.textEnc.WriteInlines(&sb, &isTitle)
		_, _ = he.textEnc.WriteInlines(&sb, &isTitle)
	} else {
		sb.Write(zn.Meta.Zid.Bytes())
	}
	head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String())))

	var body sx.ListBuilder
	body.Add(shtml.SymBody)
Changes to internal/encoder/mdenc.go.
33
34
35
36
37
38
39
40

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

40
41
42
43
44
45
46
47







-
+







// WriteZettel writes the encoded zettel to the writer.
func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
	v := newMDVisitor(w, me.lang)
	v.acceptMeta(zn.InhMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteByte('\n')
		v.b.WriteLn()
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.
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
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







-
+












-
+







		return
	}
	v.writeSpaces(4)
	lcm1 := lc - 1
	for i := 0; i < lc; i++ {
		b := vn.Content[i]
		if b != '\n' && b != '\r' {
			v.b.WriteByte(b)
			_ = v.b.WriteByte(b)
			continue
		}
		j := i + 1
		for ; j < lc; j++ {
			c := vn.Content[j]
			if c != '\n' && c != '\r' {
				break
			}
		}
		if j >= lcm1 {
			break
		}
		v.b.WriteByte('\n')
		v.b.WriteLn()
		v.writeSpaces(4)
		i = j - 1
	}
}

func (v *mdVisitor) visitRegion(rn *ast.RegionNode) {
	if rn.Kind != ast.RegionQuote {
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
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







-
+





-
+




















-
+




-
+















-
+








func (v *mdVisitor) writeNestedList(ln *ast.NestedListNode, enum string) {
	v.listInfo = append(v.listInfo, len(enum))
	regIndent := 4*len(v.listInfo) - 4
	paraIndent := regIndent + len(enum)
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte('\n')
			v.b.WriteLn()
		}
		v.writeSpaces(regIndent)
		v.b.WriteString(enum)
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				v.b.WriteLn()
				if _, ok := in.(*ast.ParaNode); ok {
					v.writeSpaces(paraIndent)
				}
			}
			ast.Walk(v, in)
		}
	}
}

func (v *mdVisitor) writeListQuote(ln *ast.NestedListNode) {
	v.listInfo = append(v.listInfo, 0)
	if len(v.listInfo) > 1 {
		return
	}

	prefix := v.listPrefix
	v.listPrefix = "> "

	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte('\n')
			v.b.WriteLn()
		}
		v.b.WriteString(v.listPrefix)
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				v.b.WriteLn()
				if _, ok := in.(*ast.ParaNode); ok {
					v.b.WriteString(v.listPrefix)
				}
			}
			ast.Walk(v, in)
		}
	}

	v.listPrefix = prefix
}

func (v *mdVisitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteByte('\n')
		v.b.WriteLn()
	}
	if l := len(v.listInfo); l > 0 {
		if v.listPrefix == "" {
			v.writeSpaces(4*l - 4 + v.listInfo[l-1])
		} else {
			v.writeSpaces(4*l - 4)
			v.b.WriteString(v.listPrefix)
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
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







-
+







-
+


-
+

-
+

-
+



















-
+

-
+







	v.writeReference(ln.Ref, ln.Inlines)
}

func (v *mdVisitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.pushAttributes(en.Attrs)
	defer v.popAttributes()

	v.b.WriteByte('!')
	_ = v.b.WriteByte('!')
	v.writeReference(en.Ref, en.Inlines)
}

func (v *mdVisitor) writeReference(ref *ast.Reference, is ast.InlineSlice) {
	if ref.State == ast.RefStateQuery {
		ast.Walk(v, &is)
	} else if len(is) > 0 {
		v.b.WriteByte('[')
		_ = v.b.WriteByte('[')
		ast.Walk(v, &is)
		v.b.WriteStrings("](", ref.String())
		v.b.WriteByte(')')
		_ = v.b.WriteByte(')')
	} else if isAutoLinkable(ref) {
		v.b.WriteByte('<')
		_ = v.b.WriteByte('<')
		v.b.WriteString(ref.String())
		v.b.WriteByte('>')
		_ = v.b.WriteByte('>')
	} else {
		s := ref.String()
		v.b.WriteStrings("[", s, "](", s, ")")
	}
}

func isAutoLinkable(ref *ast.Reference) bool {
	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
}

func (v *mdVisitor) visitFormat(fn *ast.FormatNode) {
	v.pushAttributes(fn.Attrs)
	defer v.popAttributes()

	switch fn.Kind {
	case ast.FormatEmph:
		v.b.WriteByte('*')
		_ = v.b.WriteByte('*')
		ast.Walk(v, &fn.Inlines)
		v.b.WriteByte('*')
		_ = v.b.WriteByte('*')
	case ast.FormatStrong:
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.writeQuote(fn)
	case ast.FormatMark:
363
364
365
366
367
368
369
370
371
372



373
374
375

376
377
378
379
380
381

382
383
363
364
365
366
367
368
369



370
371
372
373
374

375
376
377
378
379
380

381
382
383







-
-
-
+
+
+


-
+





-
+


	}
	v.b.WriteString(rightQ)
}

func (v *mdVisitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput:
		v.b.WriteByte('`')
		v.b.Write(ln.Content)
		v.b.WriteByte('`')
		_ = v.b.WriteByte('`')
		_, _ = v.b.Write(ln.Content)
		_ = v.b.WriteByte('`')
	case ast.LiteralComment: // ignore everything
	default:
		v.b.Write(ln.Content)
		_, _ = v.b.Write(ln.Content)
	}
}

func (v *mdVisitor) writeSpaces(n int) {
	for range n {
		v.b.WriteByte(' ')
		v.b.WriteSpace()
	}
}
Changes to internal/encoder/textenc.go.
27
28
29
30
31
32
33
34

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

50
51
52
53
54
55
56
57
58
59

60
61
62
63
64
65
66
27
28
29
30
31
32
33

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







-
+














-
+









-
+








// TextEncoder encodes just the text and ignores any formatting.
type TextEncoder struct{}

// WriteZettel writes metadata and content.
func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
	v := newTextVisitor(w)
	te.WriteMeta(&v.b, zn.InhMeta)
	_, _ = te.WriteMeta(&v.b, zn.InhMeta)
	v.visitBlockSlice(&zn.BlocksAST)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes metadata as text.
func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	buf := newEncWriter(w)
	for key, val := range m.Computed() {
		if meta.Type(key) == meta.TypeTagSet {
			writeTagSet(&buf, val.Elems())
		} else {
			buf.WriteString(string(val))
		}
		buf.WriteByte('\n')
		buf.WriteLn()
	}
	length, err := buf.Flush()
	return length, err
}

func writeTagSet(buf *encWriter, tags iter.Seq[meta.Value]) {
	first := true
	for tag := range tags {
		if !first {
			buf.WriteByte(' ')
			buf.WriteSpace()
		}
		first = false
		buf.WriteString(string(tag.CleanTag()))
	}

}

100
101
102
103
104
105
106
107

108
109
110
111
112
113
114
100
101
102
103
104
105
106

107
108
109
110
111
112
113
114







-
+







		return nil
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
		return nil
	case *ast.RegionNode:
		v.visitBlockSlice(&n.Blocks)
		if len(n.Inlines) > 0 {
			v.b.WriteByte('\n')
			v.b.WriteLn()
			ast.Walk(v, &n.Inlines)
		}
		return nil
	case *ast.NestedListNode:
		v.visitNestedList(n)
		return nil
	case *ast.DescriptionListNode:
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
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







-
+

-
+














-
+




-
+






-
-
+
+

-

















-
+











-
+







	case *ast.BLOBNode:
		return nil
	case *ast.TextNode:
		v.visitText(n.Text)
		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteByte('\n')
			v.b.WriteLn()
		} else {
			v.b.WriteByte(' ')
			v.b.WriteSpace()
		}
		return nil
	case *ast.LinkNode:
		if len(n.Inlines) > 0 {
			ast.Walk(v, &n.Inlines)
		}
		return nil
	case *ast.MarkNode:
		if len(n.Inlines) > 0 {
			ast.Walk(v, &n.Inlines)
		}
		return nil
	case *ast.FootnoteNode:
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
			v.b.WriteSpace()
		}
		// No 'return nil' to write text
	case *ast.LiteralNode:
		if n.Kind != ast.LiteralComment {
			v.b.Write(n.Content)
			_, _ = v.b.Write(n.Content)
		}
	}
	return v
}

func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) {
	if vn.Kind == ast.VerbatimComment {
		return
	if vn.Kind != ast.VerbatimComment {
		_, _ = v.b.Write(vn.Content)
	}
	v.b.Write(vn.Content)
}

func (v *textVisitor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		v.writePosChar(i, '\n')
		for j, it := range item {
			v.writePosChar(j, '\n')
			ast.Walk(v, it)
		}
	}
}

func (v *textVisitor) visitDescriptionList(dl *ast.DescriptionListNode) {
	for i, descr := range dl.Descriptions {
		v.writePosChar(i, '\n')
		ast.Walk(v, &descr.Term)
		for _, b := range descr.Descriptions {
			v.b.WriteByte('\n')
			v.b.WriteLn()
			for k, d := range b {
				v.writePosChar(k, '\n')
				ast.Walk(v, d)
			}
		}
	}
}

func (v *textVisitor) visitTable(tn *ast.TableNode) {
	if len(tn.Header) > 0 {
		v.writeRow(tn.Header)
		v.b.WriteByte('\n')
		v.b.WriteLn()
	}
	for i, row := range tn.Rows {
		v.writePosChar(i, '\n')
		v.writeRow(row)
	}
}

219
220
221
222
223
224
225
226

227
228
229
230
231
232
233
234
235
236
237
238

239
240
218
219
220
221
222
223
224

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

237
238
239







-
+











-
+


}

func (v *textVisitor) visitText(s string) {
	spaceFound := false
	for _, ch := range s {
		if input.IsSpace(ch) {
			if !spaceFound {
				v.b.WriteByte(' ')
				v.b.WriteSpace()
				spaceFound = true
			}
			continue
		}
		spaceFound = false
		v.b.WriteString(string(ch))
	}
}

func (v *textVisitor) writePosChar(pos int, ch byte) {
	if pos > 0 {
		v.b.WriteByte(ch)
		_ = v.b.WriteByte(ch)
	}
}
Changes to internal/encoder/write.go.
22
23
24
25
26
27
28
29

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

29


30
31
32
33
34
35
36







-
+
-
-







type encWriter struct {
	w      io.Writer // The io.Writer to write to
	err    error     // Collect error
	length int       // Collected length
}

// newEncWriter creates a new encWriter
func newEncWriter(w io.Writer) encWriter {
func newEncWriter(w io.Writer) encWriter { return encWriter{w: w} }
	return encWriter{w: w}
}

// Write writes the content of p.
func (w *encWriter) Write(p []byte) (l int, err error) {
	if w.err != nil {
		return 0, w.err
	}
	l, w.err = w.w.Write(p)
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
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







-
+
-
-














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



	var l int
	l, w.err = w.Write([]byte{b})
	w.length += l
	return w.err
}

// WriteBytes writes the content of bs.
func (w *encWriter) WriteBytes(bs ...byte) {
func (w *encWriter) WriteBytes(bs ...byte) { _, _ = w.Write(bs) }
	w.Write(bs)
}

// WriteBase64 writes the content of p, encoded with base64.
func (w *encWriter) WriteBase64(p []byte) {
	if w.err == nil {
		encoder := base64.NewEncoder(base64.StdEncoding, w.w)
		var l int
		l, w.err = encoder.Write(p)
		w.length += l
		err1 := encoder.Close()
		if w.err == nil {
			w.err = err1
		}
	}
}

// WriteLn writes a new line character.
func (w *encWriter) WriteLn() {
	if w.err == nil {
		w.err = w.WriteByte('\n')
	}
}

// WriteLn writes a space character.
func (w *encWriter) WriteSpace() {
	if w.err == nil {
		w.err = w.WriteByte(' ')
	}
}

// Flush returns the collected length and error.
func (w *encWriter) Flush() (int, error) { return w.length, w.err }
Changes to internal/encoder/zmkenc.go.
33
34
35
36
37
38
39
40

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

40
41
42
43
44
45
46
47







-
+







// WriteZettel writes the encoded zettel to the writer.
func (*zmkEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
	v := newZmkVisitor(w)
	v.acceptMeta(zn.InhMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteByte('\n')
		v.b.WriteLn()
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as zmk.
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
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







-
+

















-
+


-
+







	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOB(n)
	case *ast.CiteNode:
		v.visitCite(n)
	case *ast.FootnoteNode:
		v.b.WriteString("[^")
		ast.Walk(v, &n.Inlines)
		v.b.WriteByte(']')
		_ = v.b.WriteByte(']')
		v.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		v.visitMark(n)
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

func (v *zmkVisitor) visitBlockSlice(bs *ast.BlockSlice) {
	var lastWasParagraph bool
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteByte('\n')
			v.b.WriteLn()
			if lastWasParagraph && !v.inVerse {
				if _, ok := bn.(*ast.ParaNode); ok {
					v.b.WriteByte('\n')
					v.b.WriteLn()
				}
			}
		}
		ast.Walk(v, bn)
		_, lastWasParagraph = bn.(*ast.ParaNode)
	}
}
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
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







-
-
-
+
+
+

















-
+




-
+


-
+







	if vn.Kind == ast.VerbatimHTML {
		attrs = syntaxToHTML(attrs)
	}

	// TODO: scan cn.Lines to find embedded kind[0]s at beginning
	v.b.WriteString(kind)
	v.visitAttributes(attrs)
	v.b.WriteByte('\n')
	v.b.Write(vn.Content)
	v.b.WriteByte('\n')
	v.b.WriteLn()
	_, _ = v.b.Write(vn.Content)
	v.b.WriteLn()
	v.b.WriteString(kind)
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  ":::",
	ast.RegionQuote: "<<<",
	ast.RegionVerse: "\"\"\"",
}

func (v *zmkVisitor) visitRegion(rn *ast.RegionNode) {
	// Scan rn.Blocks for embedded regions to adjust length of regionCode
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %d", rn.Kind))
	}
	v.b.WriteString(kind)
	v.visitAttributes(rn.Attrs)
	v.b.WriteByte('\n')
	v.b.WriteLn()
	saveInVerse := v.inVerse
	v.inVerse = rn.Kind == ast.RegionVerse
	ast.Walk(v, &rn.Blocks)
	v.inVerse = saveInVerse
	v.b.WriteByte('\n')
	v.b.WriteLn()
	v.b.WriteString(kind)
	if len(rn.Inlines) > 0 {
		v.b.WriteByte(' ')
		v.b.WriteSpace()
		ast.Walk(v, &rn.Inlines)
	}
}

func (v *zmkVisitor) visitHeading(hn *ast.HeadingNode) {
	const headingSigns = "========= "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
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
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







-
+

-
-
+
+


-
+













-
+







-
+







	ast.NestedListQuote:     '>',
}

func (v *zmkVisitor) visitNestedList(ln *ast.NestedListNode) {
	v.prefix = append(v.prefix, mapNestedListKind[ln.Kind])
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteByte('\n')
			v.b.WriteLn()
		}
		v.b.Write(v.prefix)
		v.b.WriteByte(' ')
		_, _ = v.b.Write(v.prefix)
		v.b.WriteSpace()
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				v.b.WriteLn()
				if _, ok := in.(*ast.ParaNode); ok {
					v.writePrefixSpaces()
				}
			}
			ast.Walk(v, in)
		}
	}
	v.prefix = v.prefix[:len(v.prefix)-1]
}

func (v *zmkVisitor) writePrefixSpaces() {
	if prefixLen := len(v.prefix); prefixLen > 0 {
		for i := 0; i <= prefixLen; i++ {
			v.b.WriteByte(' ')
			v.b.WriteSpace()
		}
	}
}

func (v *zmkVisitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	for i, descr := range dn.Descriptions {
		if i > 0 {
			v.b.WriteByte('\n')
			v.b.WriteLn()
		}
		v.b.WriteString("; ")
		ast.Walk(v, &descr.Term)

		for _, b := range descr.Descriptions {
			v.b.WriteString("\n: ")
			for jj, dn := range b {
275
276
277
278
279
280
281
282

283
284
285
286

287
288
289
290
291
292
293
275
276
277
278
279
280
281

282
283
284
285

286
287
288
289
290
291
292
293







-
+



-
+







	ast.AlignCenter:  ":",
	ast.AlignRight:   ">",
}

func (v *zmkVisitor) visitTable(tn *ast.TableNode) {
	if header := tn.Header; len(header) > 0 {
		v.writeTableHeader(header, tn.Align)
		v.b.WriteByte('\n')
		v.b.WriteLn()
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte('\n')
			v.b.WriteLn()
		}
		v.writeTableRow(row, tn.Align)
	}
}

func (v *zmkVisitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
	for pos, cell := range header {
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
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







-
+










-
+




-
+







			v.b.WriteString(alignCode[colAlign])
		}
	}
}

func (v *zmkVisitor) writeTableRow(row ast.TableRow, align []ast.Alignment) {
	for pos, cell := range row {
		v.b.WriteByte('|')
		_ = v.b.WriteByte('|')
		if cell.Align != align[pos] {
			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, &cell.Inlines)
	}
}

func (v *zmkVisitor) visitBLOB(bn *ast.BLOBNode) {
	if bn.Syntax == meta.ValueSyntaxSVG {
		v.b.WriteStrings("@@@", bn.Syntax, "\n")
		v.b.Write(bn.Blob)
		_, _ = v.b.Write(bn.Blob)
		v.b.WriteString("\n@@@\n")
		return
	}
	var sb strings.Builder
	v.textEnc.WriteInlines(&sb, &bn.Description)
	_, _ = 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(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##",
)

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







-
+








-
+


-
+








-
+







-
+









-
+


-
+






-
+


-
+




















-
+

-
+

















-
-
+
+

















-
+


-
+


-
+





-
+


-
+







	v.b.WriteString(tn.Text[last:])
}

func (v *zmkVisitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteByte('\n')
		v.b.WriteLn()
	}
	v.writePrefixSpaces()
}

func (v *zmkVisitor) visitLink(ln *ast.LinkNode) {
	v.b.WriteString("[[")
	if len(ln.Inlines) > 0 {
		ast.Walk(v, &ln.Inlines)
		v.b.WriteByte('|')
		_ = v.b.WriteByte('|')
	}
	if ln.Ref.State == ast.RefStateBased {
		v.b.WriteByte('/')
		_ = v.b.WriteByte('/')
	}
	v.b.WriteStrings(ln.Ref.String(), "]]")
}

func (v *zmkVisitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.b.WriteString("{{")
	if len(en.Inlines) > 0 {
		ast.Walk(v, &en.Inlines)
		v.b.WriteByte('|')
		_ = v.b.WriteByte('|')
	}
	v.b.WriteStrings(en.Ref.String(), "}}")
}

func (v *zmkVisitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	if en.Syntax == meta.ValueSyntaxSVG {
		v.b.WriteString("@@")
		v.b.Write(en.Blob)
		_, _ = v.b.Write(en.Blob)
		v.b.WriteStrings("@@{=", en.Syntax, "}")
		return
	}
	v.b.WriteString("{{TODO: display inline BLOB}}")
}

func (v *zmkVisitor) visitCite(cn *ast.CiteNode) {
	v.b.WriteStrings("[@", cn.Key)
	if len(cn.Inlines) > 0 {
		v.b.WriteByte(' ')
		v.b.WriteSpace()
		ast.Walk(v, &cn.Inlines)
	}
	v.b.WriteByte(']')
	_ = v.b.WriteByte(']')
	v.visitAttributes(cn.Attrs)
}

func (v *zmkVisitor) visitMark(mn *ast.MarkNode) {
	v.b.WriteStrings("[!", mn.Mark)
	if len(mn.Inlines) > 0 {
		v.b.WriteByte('|')
		_ = v.b.WriteByte('|')
		ast.Walk(v, &mn.Inlines)
	}
	v.b.WriteByte(']')
	_ = v.b.WriteByte(']')

}

var mapFormatKind = map[ast.FormatKind][]byte{
	ast.FormatEmph:   []byte("__"),
	ast.FormatStrong: []byte("**"),
	ast.FormatInsert: []byte(">>"),
	ast.FormatDelete: []byte("~~"),
	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuote:  []byte(`""`),
	ast.FormatMark:   []byte("##"),
	ast.FormatSpan:   []byte("::"),
}

func (v *zmkVisitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))
	}
	v.b.Write(kind)
	_, _ = v.b.Write(kind)
	ast.Walk(v, &fn.Inlines)
	v.b.Write(kind)
	_, _ = v.b.Write(kind)
	v.visitAttributes(fn.Attrs)
}

func (v *zmkVisitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralCode:
		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:
		v.b.WriteString("%%")
		v.visitAttributes(ln.Attrs)
		v.b.WriteByte(' ')
		v.b.Write(ln.Content)
		v.b.WriteSpace()
		_, _ = v.b.Write(ln.Content)
	default:
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
	}
}

func (v *zmkVisitor) writeLiteral(code byte, a zsx.Attributes, content []byte) {
	v.b.WriteBytes(code, code)
	v.writeEscaped(string(content), code)
	v.b.WriteBytes(code, code)
	v.visitAttributes(a)
}

// visitAttributes write HTML attributes
func (v *zmkVisitor) visitAttributes(a zsx.Attributes) {
	if a.IsEmpty() {
		return
	}
	v.b.WriteByte('{')
	_ = v.b.WriteByte('{')
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteByte(' ')
			v.b.WriteSpace()
		}
		if k == "-" {
			v.b.WriteByte('-')
			_ = v.b.WriteByte('-')
			continue
		}
		v.b.WriteString(k)
		if vl := a[k]; len(vl) > 0 {
			v.b.WriteStrings("=\"", vl)
			v.b.WriteByte('"')
			_ = v.b.WriteByte('"')
		}
	}
	v.b.WriteByte('}')
	_ = v.b.WriteByte('}')
}

func (v *zmkVisitor) writeEscaped(s string, toEscape byte) {
	last := 0
	for i := range len(s) {
		if b := s[i]; b == toEscape || b == '\\' {
			v.b.WriteString(s[last:i])
Changes to internal/evaluator/evaluator.go.
65
66
67
68
69
70
71
72

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

72
73
74
75
76
77
78
79







-
+







		if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimCode {
			if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.ValueSyntaxSxn {
				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)
						_, _ = sxbuiltins.Print(&buf, obj)
						result[i] = &ast.VerbatimNode{
							Kind:    ast.VerbatimCode,
							Attrs:   zsx.Attributes{"": classAttr},
							Content: buf.Bytes(),
						}
					}
					return result
Changes to internal/kernel/cfg.go.
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
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







+

-
+

-
+
+
+
+




-
+









+

-
+

-
+
+
+
+







	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() {
		var setErr error
		if val, ok := m.Get(key); ok {
			cs.SetConfig(key, string(val))
			setErr = cs.SetConfig(key, string(val))
		} else if defVal, defFound := cs.orig.Get(key); defFound {
			cs.SetConfig(key, string(defVal))
			setErr = cs.SetConfig(key, string(defVal))
		}
		if err == nil && setErr != nil && setErr != errAlreadyFrozen {
			err = fmt.Errorf("%w (key=%q)", setErr, key)
		}
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
	return err
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ZidConfiguration {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() {
			cs.mxService.RLock()
			mgr := cs.manager
			cs.mxService.RUnlock()
			var err error
			if mgr != nil {
				cs.doUpdate(mgr)
				err = cs.doUpdate(mgr)
			} else {
				cs.doUpdate(ci.Box)
				err = cs.doUpdate(ci.Box)
			}
			if err != nil {
				cs.logger.Error().Err(err).Msg("update config")
			}
		}()
	}
}

// --- config.Config

Changes to internal/kernel/cmd.go.
58
59
60
61
62
63
64
65

66
67
68


69
70
71

72
73
74
75
76
77
78
58
59
60
61
62
63
64

65
66


67
68
69
70

71
72
73
74
75
76
77
78







-
+

-
-
+
+


-
+







	sess.println("Unknown command:", cmd, strings.Join(args, " "))
	sess.println("-- Enter 'help' go get a list of valid commands.")
	return true
}

func (sess *cmdSession) println(args ...string) {
	if len(args) > 0 {
		io.WriteString(sess.w, args[0])
		_, _ = io.WriteString(sess.w, args[0])
		for _, arg := range args[1:] {
			io.WriteString(sess.w, " ")
			io.WriteString(sess.w, arg)
			_, _ = io.WriteString(sess.w, " ")
			_, _ = io.WriteString(sess.w, arg)
		}
	}
	sess.w.Write(sess.eol)
	_, _ = sess.w.Write(sess.eol)
}

func (sess *cmdSession) usage(cmd, val string) {
	sess.println("Usage:", cmd, val)
}

func (sess *cmdSession) printTable(table [][]string) {
106
107
108
109
110
111
112
113

114
115

116
117

118
119
120
121
122
123
124
106
107
108
109
110
111
112

113
114

115
116

117
118
119
120
121
122
123
124







-
+

-
+

-
+







		}
	}
	return maxLen
}

func (sess *cmdSession) printRow(row []string, maxLen []int, prefix, delim string, pad rune) {
	for colno, column := range row {
		io.WriteString(sess.w, prefix)
		_, _ = io.WriteString(sess.w, prefix)
		prefix = delim
		io.WriteString(sess.w, strfun.JustifyLeft(column, maxLen[colno], pad))
		_, _ = io.WriteString(sess.w, strfun.JustifyLeft(column, maxLen[colno], pad))
	}
	sess.w.Write(sess.eol)
	_, _ = sess.w.Write(sess.eol)
}

func splitLine(line string) (string, []string) {
	s := strings.Fields(line)
	if len(s) == 0 {
		return "", nil
	}
500
501
502
503
504
505
506
507



508
509
510
511
512
513
514
500
501
502
503
504
505
506

507
508
509
510
511
512
513
514
515
516







-
+
+
+







	sess.kern.dumpIndex(sess.w)
	return true
}

func cmdRefresh(sess *cmdSession, _ string, _ []string) bool {
	kern := sess.kern
	kern.logger.Mandatory().Msg("Refresh")
	kern.box.Refresh()
	if err := kern.box.Refresh(); err != nil {
		kern.logger.Error().Err(err).Msg("refresh")
	}
	return true
}

func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.usage(cmd, "RECOVER")
		sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.")
Changes to internal/kernel/config.go.
57
58
59
60
61
62
63
64

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

64
65
66
67
68
69
70
71







-
+







			text = text + " (list)"
		}
		result = append(result, serviceConfigDescription{Key: k, Descr: text})
	}
	return result
}

var errAlreadyFrozen = errors.New("value not allowed to be set")
var errAlreadyFrozen = errors.New("value frozen")

func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc {
	return func(val string) (any, error) {
		if cfg.frozen {
			return nil, errAlreadyFrozen
		}
		return parse(val)
Changes to internal/kernel/kernel.go.
202
203
204
205
206
207
208


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







+
+







)

// Constants for web service keys.
const (
	WebAssetDir          = "asset-dir"
	WebBaseURL           = "base-url"
	WebListenAddress     = "listen"
	WebLoopbackIdent     = "loopback-ident"
	WebLoopbackZid       = "loopback-zid"
	WebPersistentCookie  = "persistent"
	WebProfiling         = "profiling"
	WebMaxRequestSize    = "max-request-size"
	WebSecureCookie      = "secure"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
251
252
253
254
255
256
257
258
259
260



261
262
263
264
265
266
267
253
254
255
256
257
258
259



260
261
262
263
264
265
266
267
268
269







-
-
-
+
+
+







	defaultNormalLogLevel = logger.InfoLevel
	defaultSimpleLogLevel = logger.ErrorLevel
)

// Setup sets the most basic data of a software: its name, its version,
// and when the version was created.
func (kern *Kernel) Setup(progname, version string, versionTime time.Time) {
	kern.SetConfig(CoreService, CoreProgname, progname)
	kern.SetConfig(CoreService, CoreVersion, version)
	kern.SetConfig(CoreService, CoreVTime, versionTime.Local().Format(id.TimestampLayout))
	_ = kern.SetConfig(CoreService, CoreProgname, progname)
	_ = kern.SetConfig(CoreService, CoreVersion, version)
	_ = kern.SetConfig(CoreService, CoreVTime, versionTime.Local().Format(id.TimestampLayout))
}

// Start the service.
func (kern *Kernel) Start(headline, lineServer bool, configFilename string) {
	for _, srvD := range kern.srvs {
		srvD.srv.Freeze()
	}
276
277
278
279
280
281
282
283

284
285
286
287
288
289
290
278
279
280
281
282
283
284

285
286
287
288
289
290
291
292







-
+







		if strSig := sig.String(); strSig != "" {
			kern.logger.Info().Str("signal", strSig).Msg("Shut down Zettelstore")
		}
		kern.doShutdown()
		kern.wg.Done()
	}()

	kern.StartService(KernelService)
	_ = kern.StartService(KernelService)
	if headline {
		logger := kern.logger
		logger.Mandatory().Msg(fmt.Sprintf(
			"%v %v (%v@%v/%v)",
			kern.core.GetCurConfig(CoreProgname),
			kern.core.GetCurConfig(CoreVersion),
			kern.core.GetCurConfig(CoreGoVersion),
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
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







-
+











-
+







			logger.Info().Msg("Read-only mode")
		}
	}
	if lineServer {
		port := kern.core.GetNextConfig(CorePort).(int)
		if port > 0 {
			listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
			startLineServer(kern, listenAddr)
			_ = startLineServer(kern, listenAddr)
		}
	}
}

func (kern *Kernel) doShutdown() {
	kern.stopService(KernelService) // Will stop all other services.
}

// WaitForShutdown blocks the call until Shutdown is called.
func (kern *Kernel) WaitForShutdown() {
	kern.wg.Wait()
	kern.doStopProfiling()
	_ = kern.doStopProfiling()
}

// --- Shutdown operation ----------------------------------------------------

// Shutdown the service. Waits for all concurrent activities to stop.
func (kern *Kernel) Shutdown(silent bool) {
	kern.logger.Trace().Msg("Shutdown")
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
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







-
+
-
-
+




















-
+
-







		return errProfileInWork
	}
	if profileName == ProfileCPU {
		f, err := os.Create(fileName)
		if err != nil {
			return err
		}
		err = pprof.StartCPUProfile(f)
		if err = pprof.StartCPUProfile(f); err != nil {
		if err != nil {
			f.Close()
			_ = f.Close()
			return err
		}
		kern.profileName = profileName
		kern.fileName = fileName
		kern.profileFile = f
		return nil
	}
	profile := pprof.Lookup(profileName)
	if profile == nil {
		return errProfileNotFound
	}
	f, err := os.Create(fileName)
	if err != nil {
		return err
	}
	kern.profileName = profileName
	kern.fileName = fileName
	kern.profile = profile
	kern.profileFile = f
	runtime.GC() // get up-to-date statistics
	profile.WriteTo(f, 0)
	return profile.WriteTo(f, 0)
	return nil
}

// StopProfiling stops the current profiling and writes the result to
// the file, which was named during StartProfiling().
// It will always be called before the software stops its operations.
func (kern *Kernel) StopProfiling() error {
	kern.mx.Lock()
Changes to internal/kernel/server.go.
43
44
45
46
47
48
49
50

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

50
51
52
53
54
55
56
57







-
+







		if err != nil {
			// handle error
			kern.logger.Error().Err(err).Msg("Unable to accept connection")
			break
		}
		go handleLineConnection(conn, kern)
	}
	ln.Close()
	_ = ln.Close()
}

func handleLineConnection(conn net.Conn, kern *Kernel) {
	// Something may panic. Ensure a running connection.
	defer func() {
		if ri := recover(); ri != nil {
			kern.LogRecover("LineConn", ri)
65
66
67
68
69
70
71
72

73
65
66
67
68
69
70
71

72
73







-
+

	s := bufio.NewScanner(conn)
	for s.Scan() {
		line := s.Text()
		if !cmds.executeLine(line) {
			break
		}
	}
	conn.Close()
	_ = conn.Close()
}
Changes to internal/kernel/web.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







+
+







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

	"t73f.de/r/zsc/domain/id"

	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/web/server"
)

type webService struct {
	srvConfig
74
75
76
77
78
79
80


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







+
+







				ap, err := netip.ParseAddrPort(val)
				if err != nil {
					return "", err
				}
				return ap.String(), nil
			},
			true},
		WebLoopbackIdent:    {"Loopback user ident", ws.noFrozen(parseString), true},
		WebLoopbackZid:      {"Loopback user zettel identifier", ws.noFrozen(parseInvalidZid), true},
		WebMaxRequestSize:   {"Max Request Size", parseInt64, true},
		WebPersistentCookie: {"Persistent cookie", parseBool, true},
		WebProfiling:        {"Runtime profiling", parseBool, true},
		WebSecureCookie:     {"Secure cookie", parseBool, true},
		WebTokenLifetimeAPI: {
			"Token lifetime API",
			makeDurationParser(10*time.Minute, 0, 1*time.Hour),
103
104
105
106
107
108
109


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







+
+







			true,
		},
	}
	ws.next = interfaceMap{
		WebAssetDir:          "",
		WebBaseURL:           "http://127.0.0.1:23123/",
		WebListenAddress:     "127.0.0.1:23123",
		WebLoopbackIdent:     "",
		WebLoopbackZid:       id.Invalid,
		WebMaxRequestSize:    int64(16 * 1024 * 1024),
		WebPersistentCookie:  false,
		WebSecureCookie:      true,
		WebProfiling:         false,
		WebTokenLifetimeAPI:  1 * time.Hour,
		WebTokenLifetimeHTML: 10 * time.Minute,
		WebURLPrefix:         "/",
136
137
138
139
140
141
142


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







+
+







var errWrongBasePrefix = errors.New(WebURLPrefix + " does not match " + WebBaseURL)

func (ws *webService) GetLogger() *logger.Logger { return ws.logger }

func (ws *webService) Start(kern *Kernel) error {
	baseURL := ws.GetNextConfig(WebBaseURL).(string)
	listenAddr := ws.GetNextConfig(WebListenAddress).(string)
	loopbackIdent := ws.GetNextConfig(WebLoopbackIdent).(string)
	loopbackZid := ws.GetNextConfig(WebLoopbackZid).(id.Zid)
	urlPrefix := ws.GetNextConfig(WebURLPrefix).(string)
	persistentCookie := ws.GetNextConfig(WebPersistentCookie).(bool)
	secureCookie := ws.GetNextConfig(WebSecureCookie).(bool)
	profile := ws.GetNextConfig(WebProfiling).(bool)
	maxRequestSize := max(ws.GetNextConfig(WebMaxRequestSize).(int64), 1024)

	if !strings.HasSuffix(baseURL, urlPrefix) {
159
160
161
162
163
164
165


166
167
168
169
170
171
172
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182







+
+







	sd := server.ConfigData{
		Log:              ws.logger,
		ListenAddr:       listenAddr,
		BaseURL:          baseURL,
		URLPrefix:        urlPrefix,
		MaxRequestSize:   maxRequestSize,
		Auth:             kern.auth.manager,
		LoopbackIdent:    loopbackIdent,
		LoopbackZid:      loopbackZid,
		PersistentCookie: persistentCookie,
		SecureCookie:     secureCookie,
		Profiling:        profile,
	}
	srvw := server.New(sd)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg)
	if err != nil {
Changes to internal/logger/message.go.
15
16
17
18
19
20
21

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







+








import (
	"context"
	"net/http"
	"strconv"
	"sync"

	"t73f.de/r/webs/ip"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
)

// Message presents a message to log.
type Message struct {
	logger *Logger
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
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







-
-
-
+
+
+
+


-
-
+
-
-










-
+








				}
			}
		}
	}
	return m
}

// HTTPIP adds the IP address of a HTTP request to the message.
func (m *Message) HTTPIP(r *http.Request) *Message {
	if r == nil {
// RemoteAddr adds the remote address of an HTTP request to the message.
func (m *Message) RemoteAddr(r *http.Request) *Message {
	addr := ip.GetRemoteAddr(r)
	if addr == "" {
		return m
	}
	if from := r.Header.Get("X-Forwarded-For"); from != "" {
		return m.Str("ip", from)
	return m.Str("remote", addr)
	}
	return m.Str("IP", r.RemoteAddr)
}

// Zid adds a zettel identifier to the full message
func (m *Message) Zid(zid id.Zid) *Message {
	return m.Bytes("zid", zid.Bytes())
}

// Msg add the given text to the message and writes it to the log.
func (m *Message) Msg(text string) {
	if m.Enabled() {
		m.logger.writeMessage(m.level, text, m.buf)
		_ = m.logger.writeMessage(m.level, text, m.buf)
		recycleMessage(m)
	}
}

// Child creates a child logger with context of this message.
func (m *Message) Child() *Logger {
	return newFromMessage(m)
}
Changes to internal/parser/cleaner.go.
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
159
160
161
162
163
164
165
















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
				return newID
			}
		}
	}
	cv.ids[id] = node
	return id
}

// CleanInlineLinks removes all links and footnote node from the given inline slice.
func CleanInlineLinks(is *ast.InlineSlice) { ast.Walk(&cleanLinks{}, is) }

type cleanLinks struct{}

func (cl *cleanLinks) Visit(node ast.Node) ast.Visitor {
	ins, ok := node.(*ast.InlineSlice)
	if !ok {
		return cl
	}
	for _, in := range *ins {
		ast.Walk(cl, in)
	}
	if hasNoLinks(*ins) {
		return nil
	}

	result := make(ast.InlineSlice, 0, len(*ins))
	for _, in := range *ins {
		switch n := in.(type) {
		case *ast.LinkNode:
			result = append(result, n.Inlines...)
		case *ast.FootnoteNode: // Do nothing
		default:
			result = append(result, n)
		}
	}
	*ins = result
	return nil
}

func hasNoLinks(ins ast.InlineSlice) bool {
	for _, in := range ins {
		switch in.(type) {
		case *ast.LinkNode, *ast.FootnoteNode:
			return false
		}
	}
	return true
}
Changes to internal/query/print.go.
92
93
94
95
96
97
98
99

100
101
102
103
104
105


106
107
108

109
110
111
112
113
114
115
92
93
94
95
96
97
98

99
100
101
102
103


104
105
106
107

108
109
110
111
112
113
114
115







-
+




-
-
+
+


-
+







	space bool
}

var bsSpace = []byte{' '}

func (pe *PrintEnv) printSpace() {
	if pe.space {
		pe.w.Write(bsSpace)
		_, _ = pe.w.Write(bsSpace)
		return
	}
	pe.space = true
}
func (pe *PrintEnv) write(ch byte)        { pe.w.Write([]byte{ch}) }
func (pe *PrintEnv) writeString(s string) { io.WriteString(pe.w, s) }
func (pe *PrintEnv) write(ch byte)        { _, _ = pe.w.Write([]byte{ch}) }
func (pe *PrintEnv) writeString(s string) { _, _ = io.WriteString(pe.w, s) }
func (pe *PrintEnv) writeStrings(sSeq ...string) {
	for _, s := range sSeq {
		io.WriteString(pe.w, s)
		_, _ = io.WriteString(pe.w, s)
	}
}

func (pe *PrintEnv) printZids(zids []id.Zid) {
	for i, zid := range zids {
		if i > 0 {
			pe.printSpace()
Changes to internal/usecase/authenticate.go.
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
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







-
+







-
+











-
+









-
+







// Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it
// for other purposes.
func (uc *Authenticate) Run(ctx context.Context, r *http.Request, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) {
	identMeta, err := uc.ucGetUser.Run(ctx, ident)
	defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond)

	if identMeta == nil || err != nil {
		uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found")
		uc.log.Info().Str("ident", ident).Err(err).RemoteAddr(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 err != nil {
			uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials")
			uc.log.Info().Str("ident", ident).Err(err).RemoteAddr(r).Msg("Error while comparing credentials")
			return nil, err
		}
		if ok {
			token, err2 := uc.token.GetToken(identMeta, d, k)
			if err2 != nil {
				uc.log.Info().Str("ident", ident).Err(err).Msg("Unable to produce authentication token")
				return nil, err2
			}
			uc.log.Info().Str("user", ident).Msg("Successful")
			return token, nil
		}
		uc.log.Info().Str("ident", ident).HTTPIP(r).Msg("Credentials don't match")
		uc.log.Info().Str("ident", ident).RemoteAddr(r).Msg("Credentials don't match")
		return nil, nil
	}
	uc.log.Info().Str("ident", ident).Msg("No credential stored")
	compensateCompare()
	return nil, nil
}

// compensateCompare if normal comapare is not possible, to avoid timing hints.
func compensateCompare() {
	cred.CompareHashAndCredential(
	_, _ = cred.CompareHashAndCredential(
		"$2a$10$WHcSO3G9afJ3zlOYQR1suuf83bCXED2jmzjti/MH4YH4l2mivDuze", id.Invalid, "", "")
}

// addDelay after credential checking to allow some CPU time for other tasks.
// durDelay is the normal delay, if time spend for checking is smaller than
// the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added.
func addDelay(start time.Time, durDelay, minDelay time.Duration) {
Changes to internal/web/adapter/api/request.go.
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
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







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

-
+













-
+







func getPart(q url.Values, defPart partType) partType {
	if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok {
		return part
	}
	return defPart
}

func (p partType) String() string {
	switch p {
	case partMeta:
		return "meta"
	case partContent:
		return "content"
	case partZettel:
		return "zettel"
	}
	return ""
}

func (p partType) DefString(defPart partType) string {
	if p == defPart {
		return ""
	}
	return p.String()
}

func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	defer r.Body.Close()
	defer func() { _ = r.Body.Close() }()
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return zettel.Zettel{}, err
	}
	inp := input.NewInput(b)
	m := meta.NewFromInput(zid, inp)
	return zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent(inp.Src[inp.Pos:]),
	}, nil
}

func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	defer r.Body.Close()
	defer func() { _ = r.Body.Close() }()
	rdr := sxreader.MakeReader(r.Body)
	obj, err := rdr.Read()
	if err != nil {
		return zettel.Zettel{}, err
	}
	zd, err := sexp.ParseZettel(obj)
	if err != nil {
Changes to internal/web/adapter/webui/favicon.go.
28
29
30
31
32
33
34
35

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

35
36
37
38
39
40
41
42







-
+







		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			wui.log.Debug().Err(err).Msg("Favicon not found")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		defer f.Close()
		defer func() { _ = f.Close() }()

		data, err := io.ReadAll(f)
		if err != nil {
			wui.log.Error().Err(err).Msg("Unable to read favicon data")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
Changes to internal/web/adapter/webui/forms.go.
103
104
105
106
107
108
109
110

111
112
113
114
115
116
117
103
104
105
106
107
108
109

110
111
112
113
114
115
116
117







-
+







	}
	return nil
}

func uploadedContent(r *http.Request, m *meta.Meta) ([]byte, *meta.Meta) {
	file, fh, err := r.FormFile("file")
	if file != nil {
		defer file.Close()
		defer func() { _ = file.Close() }()
		if err == nil {
			data, err2 := io.ReadAll(file)
			if err2 != nil {
				return nil, m
			}
			if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 {
				ct := cts[0]
Changes to internal/web/adapter/webui/htmlgen.go.
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
271
272
273
274
275
276
277








278
279
280
281
282
283
284







-
-
-
-
-
-
-
-







	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil, nil, err
	}
	return sh, shtml.Endnotes(&env), nil
}

// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair {
	if is == nil || len(*is) == 0 {
		return nil
	}
	return g.nodeSxHTML(is)
}

func (g *htmlGenerator) nodeSxHTML(node ast.Node) *sx.Pair {
	sz := g.tx.GetSz(node)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sz, &env)
	if err != nil {
		return nil
	}
Changes to internal/web/adapter/webui/htmlmeta.go.
110
111
112
113
114
115
116
117
118


119

120
121
122
123
124
125
126
110
111
112
113
114
115
116

117
118
119

120
121
122
123
124
125
126
127







-

+
+
-
+







		return lb.List()
	}
	return nil
}

func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
	var lb sx.ListBuilder
	lb.Add(shtml.SymSPAN)
	for i, tag := range tags {
		if i == 0 {
			lb.Add(shtml.SymSPAN)
		if i > 0 {
		} else if i > 0 {
			lb.Add(space)
		}
		lb.Add(wui.transformKeyValueText(key, meta.Value(tag), tag))
	}
	if len(tags) > 1 {
		lb.AddN(space, wui.transformKeyValuesText(key, tags, "(all)"))
	}
Changes to internal/web/adapter/webui/sxn_code.go.
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







-







		return z.Meta, nil
	}
	dg := buildSxnCodeDigraph(ctx, id.ZidSxnStart, getMeta)
	if dg == nil {
		return nil, wui.rootBinding, nil
	}
	dg = dg.AddVertex(id.ZidSxnBase).AddEdge(id.ZidSxnStart, id.ZidSxnBase)
	dg = dg.AddVertex(id.ZidSxnPrelude).AddEdge(id.ZidSxnBase, id.ZidSxnPrelude)
	dg = dg.TransitiveClosure(id.ZidSxnStart)

	if zid, isDAG := dg.IsDAG(); !isDAG {
		return nil, nil, fmt.Errorf("zettel %v is part of a dependency cycle", zid)
	}
	bind := wui.rootBinding.MakeChildBinding("zettel", 128)
	for _, zid := range dg.SortReverse() {
Changes to internal/web/adapter/webui/template.go.
37
38
39
40
41
42
43
44


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

44
45
46
47


48
49
50
51
52
53
54







-
+
+


-
-







	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/web/server"
	"zettelstore.de/z/internal/zettel"
)

func (wui *WebUI) createRenderBinding() *sxeval.Binding {
	root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3)
	root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 32)
	_ = sxbuiltins.LoadPrelude(root)
	_ = sxeval.BindSpecials(root, specials...)
	_ = sxeval.BindBuiltins(root, builtins...)
	_ = root.Bind(sx.MakeSymbol("NIL"), sx.Nil())
	_ = root.Bind(sx.MakeSymbol("T"), sx.MakeSymbol("T"))
	_ = sxeval.BindBuiltins(root,
		&sxeval.Builtin{
			Name:     "url-to-html",
			MinArity: 1,
			MaxArity: 1,
			TestPure: sxeval.AssertPure,
			Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Binding) (sx.Object, error) {
98
99
100
101
102
103
104
105
106
107
108
109






110
111
112
113
114
115
116
97
98
99
100
101
102
103





104
105
106
107
108
109
110
111
112
113
114
115
116







-
-
-
-
-
+
+
+
+
+
+








var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefVarS,                     // defvar
		&sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda
		&sxbuiltins.SetXS,     // set!
		&sxbuiltins.IfS,       // if
		&sxbuiltins.BeginS,    // begin
		&sxbuiltins.DefMacroS, // defmacro
		&sxbuiltins.LetS,      // let
		&sxbuiltins.SetXS,                      // set!
		&sxbuiltins.IfS,                        // if
		&sxbuiltins.BeginS,                     // begin
		&sxbuiltins.DefMacroS,                  // defmacro
		&sxbuiltins.LetS, &sxbuiltins.LetStarS, // let, let*
		&sxbuiltins.AndS, &sxbuiltins.OrS, // and, or
	}
	builtins = []*sxeval.Builtin{
		&sxbuiltins.Equal,                // =
		&sxbuiltins.NumGreater,           // >
		&sxbuiltins.NullP,                // null?
		&sxbuiltins.PairP,                // pair?
		&sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr
405
406
407
408
409
410
411
412



413
414
415
416
417
418
419
405
406
407
408
409
410
411

412
413
414
415
416
417
418
419
420
421







-
+
+
+







	return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, bind)
}
func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, bind *sxeval.Binding) error {
	detailObj, err := wui.evalSxnTemplate(ctx, templateID, bind)
	if err != nil {
		return err
	}
	bind.Bind(symDetail, detailObj)
	if err = bind.Bind(symDetail, detailObj); err != nil {
		return err
	}

	pageObj, err := wui.evalSxnTemplate(ctx, id.ZidBaseTemplate, bind)
	if err != nil {
		return err
	}
	if msg := wui.log.Debug(); msg != nil {
		// pageObj.String() can be expensive to calculate.
452
453
454
455
456
457
458
459

460
461
462
463
464
465
466
454
455
456
457
458
459
460

461
462
463
464
465
466
467
468







-
+







	if errSx == nil {
		return
	}
	wui.log.Error().Err(errSx).Msg("while rendering error message")

	// if errBind != nil, the HTTP header was not written
	wui.prepareAndWriteHeader(w, http.StatusInternalServerError)
	fmt.Fprintf(
	_, _ = fmt.Fprintf(
		w,
		`<!DOCTYPE html>
<html>
<head><title>Internal server error</title></head>
<body>
<h1>Internal server error</h1>
<p>When generating error code %d with message:</p><pre>%v</pre><p>an error occured:</p><pre>%v</pre>
Changes to internal/web/server/http.go.
16
17
18
19
20
21
22

23

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







+

+







import (
	"context"
	"net"
	"net/http"
	"time"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/logger"
)

type webServer struct {
	log              *logger.Logger
	baseURL          string
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
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







+
+



















+
+







type ConfigData struct {
	Log              *logger.Logger
	ListenAddr       string
	BaseURL          string
	URLPrefix        string
	MaxRequestSize   int64
	Auth             auth.TokenManager
	LoopbackIdent    string
	LoopbackZid      id.Zid
	PersistentCookie bool
	SecureCookie     bool
	Profiling        bool
}

// New creates a new web server.
func New(sd ConfigData) Server {
	srv := webServer{
		log:              sd.Log,
		baseURL:          sd.BaseURL,
		persistentCookie: sd.PersistentCookie,
		secureCookie:     sd.SecureCookie,
	}

	rd := routerData{
		log:            sd.Log,
		urlPrefix:      sd.URLPrefix,
		maxRequestSize: sd.MaxRequestSize,
		auth:           sd.Auth,
		loopbackIdent:  sd.LoopbackIdent,
		loopbackZid:    sd.LoopbackZid,
		profiling:      sd.Profiling,
	}
	srv.router.initializeRouter(rd)
	srv.httpServer.initializeHTTPServer(sd.ListenAddr, &srv.router)
	return &srv
}

166
167
168
169
170
171
172
173

174
175
176
177
178
179
180
181
182

183
172
173
174
175
176
177
178

179
180
181
182
183
184
185
186
187

188
189







-
+








-
+

// start the web server, but does not wait for its completion.
func (srv *httpServer) start() error {
	ln, err := net.Listen("tcp", srv.Addr)
	if err != nil {
		return err
	}

	go func() { srv.Serve(ln) }()
	go func() { _ = srv.Serve(ln) }()
	return nil
}

// stop the web server.
func (srv *httpServer) stop() {
	ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
	defer cancel()

	srv.Shutdown(ctx)
	_ = srv.Shutdown(ctx)
}
Changes to internal/web/server/router.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
64
65
66
67
68


69
70
71
72
73
74
75
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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







+
+
+



















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







+
+








+
+







	"io"
	"net/http"
	"net/http/pprof"
	"regexp"
	rtprf "runtime/pprof"
	"strings"

	"t73f.de/r/webs/ip"
	"t73f.de/r/zsc/domain/id"

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/logger"
)

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

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

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

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

// initializeRouter creates a new, empty router with the given root handler.
func (rt *httpRouter) initializeRouter(rd routerData) {
	rt.log = rd.log
	rt.urlPrefix = rd.urlPrefix
	rt.auth = rd.auth
	rt.loopbackIdent = rd.loopbackIdent
	rt.loopbackZid = rd.loopbackZid
	rt.minKey = 255
	rt.maxKey = 0
	rt.reURL = regexp.MustCompile("^$")
	rt.mux = http.NewServeMux()
	rt.maxReqSize = rd.maxRequestSize

	if rd.profiling {
131
132
133
134
135
136
137
138

139
140
141
142
143
144
145
140
141
142
143
144
145
146

147
148
149
150
151
152
153
154







-
+







}

func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	var withDebug bool
	if msg := rt.log.Debug(); msg.Enabled() {
		withDebug = true
		w = &traceResponseWriter{original: w}
		msg.Str("method", r.Method).Str("uri", r.RequestURI).HTTPIP(r).Msg("ServeHTTP")
		msg.Str("method", r.Method).Str("uri", r.RequestURI).RemoteAddr(r).Msg("ServeHTTP")
	}

	if prefixLen := len(rt.urlPrefix); prefixLen > 1 {
		if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix {
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			if withDebug {
				rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP/prefix")
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
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







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













-
+


-


-
+







}

func (rt *httpRouter) addUserContext(r *http.Request) *http.Request {
	if rt.ur == nil {
		// No auth needed
		return r
	}
	ctx := r.Context()

	if rt.loopbackZid.IsValid() {
		if remoteAddr := ip.GetRemoteAddr(r); ip.IsLoopbackAddr(remoteAddr) {
			if user, err := rt.ur.GetUser(ctx, rt.loopbackZid, rt.loopbackIdent); err == nil {
				if user != nil {
					return r.WithContext(updateContext(ctx, user, nil))
				}
				rt.log.Error().Str("loopback-ident", rt.loopbackIdent).Msg("No match to loopback-zid")
			}
		}
	}

	k := auth.KindAPI
	t := getHeaderToken(r)
	if len(t) == 0 {
		rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP
		k = auth.KindwebUI
		t = getSessionToken(r)
	}
	if len(t) == 0 {
		rt.log.Debug().Msg("no auth token found in request") // IP already logged: ServeHTTP
		return r
	}
	tokenData, err := rt.auth.CheckToken(t, k)
	if err != nil {
		rt.log.Info().Err(err).HTTPIP(r).Msg("invalid auth token")
		rt.log.Info().Err(err).RemoteAddr(r).Msg("invalid auth token")
		return r
	}
	ctx := r.Context()
	user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident)
	if err != nil {
		rt.log.Info().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found")
		rt.log.Info().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).RemoteAddr(r).Msg("auth user not found")
		return r
	}
	return r.WithContext(updateContext(ctx, user, &tokenData))
}

func getSessionToken(r *http.Request) []byte {
	cookie, err := r.Cookie(sessionName)
Changes to tests/client/client_test.go.
46
47
48
49
50
51
52
53
54
55
56




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




53
54
55
56
57
58
59
60
61
62
63







-
-
-
-
+
+
+
+







		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 59
		configRoleZettel = 37
		writerZettel     = ownerZettel - 25
		readerZettel     = ownerZettel - 25
		ownerZettel      = 58
		configRoleZettel = 36
		writerZettel     = ownerZettel - 24
		readerZettel     = ownerZettel - 24
		creatorZettel    = 11
		publicZettel     = 6
	)

	testdata := []struct {
		user string
		exp  int
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
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







-
+











-
+







	search := "emoji" + api.ActionSeparator + api.RedirectAction
	ub := c.NewURLBuilder('z').AppendQuery(search)
	respRedirect, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respRedirect.Body.Close()
	defer func() { _ = respRedirect.Body.Close() }()
	bodyRedirect, err := io.ReadAll(respRedirect.Body)
	if err != nil {
		t.Error(err)
		return
	}
	ub.ClearQuery().SetZid(id.ZidEmoji)
	respEmoji, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respEmoji.Body.Close()
	defer func() { _ = respEmoji.Body.Close() }()
	bodyEmoji, err := io.ReadAll(respEmoji.Body)
	if err != nil {
		t.Error(err)
		return
	}
	if !slices.Equal(bodyRedirect, bodyEmoji) {
		t.Error("Wrong redirect")
Changes to tests/markdown_test.go.
78
79
80
81
82
83
84
85

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

98
99
100
101
102
103

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







-
+











-
+





-
+









-
+




















-
+






}

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: meta.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)
		_, _ = zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()

		testID = tc.Example*100 + 2
		secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &secondAst)
		_, _ = 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.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &thirdAst)
		_, _ = 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"},
	}
	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)
		_, _ = 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.
35
36
37
38
39
40
41
42

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

42
43
44
45
46
47
48
49







-
+








func getNaughtyStrings() (result []string, err error) {
	fpath := filepath.Join("..", "testdata", "naughty", "blns.txt")
	file, err := os.Open(fpath)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	defer func() { _ = file.Close() }()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		if text := scanner.Text(); text != "" && text[0] != '#' {
			result = append(result, text)
		}
	}
	return result, scanner.Err()
Changes to tests/regression_test.go.
91
92
93
94
95
96
97
98
99




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

98
99
100
101
102
103
104
105
106
107
108
109







-

+
+
+
+







}

func resultFile(file string) (data string, err error) {
	f, err := os.Open(file)
	if err != nil {
		return "", err
	}
	defer f.Close()
	src, err := io.ReadAll(f)
	err2 := f.Close()
	if err == nil {
		err = err2
	}
	return string(src), err
}

func checkFileContent(t *testing.T, filename, gotContent string) {
	t.Helper()
	wantContent, err := resultFile(filename)
	if err != nil {
123
124
125
126
127
128
129
130

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

133
134
135
136
137
138
139
140







-
+







}

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 {
		var sf strings.Builder
		enc.WriteMeta(&sf, zn.Meta)
		_, _ = enc.WriteMeta(&sf, zn.Meta)
		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) {
Changes to tools/build/build.go.
166
167
168
169
170
171
172
173

174
175

176
177
178
179
180
181
182
166
167
168
169
170
171
172

173
174

175
176
177
178
179
180
181
182







-
+

-
+







		return err
	}
	zipName := filepath.Join(path, "manual-"+base+".zip")
	zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	defer zipFile.Close()
	defer func() { _ = zipFile.Close() }()
	zipWriter := zip.NewWriter(zipFile)
	defer zipWriter.Close()
	defer func() { _ = zipWriter.Close() }()

	for _, entry := range entries {
		if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
			return err
		}
	}
	return nil
200
201
202
203
204
205
206
207

208
209
210
211
212
213
214
200
201
202
203
204
205
206

207
208
209
210
211
212
213
214







-
+







	if err != nil {
		return err
	}
	manualFile, err := os.Open(filepath.Join(path, name))
	if err != nil {
		return err
	}
	defer manualFile.Close()
	defer func() { _ = manualFile.Close() }()

	if name != versionZid+".zettel" {
		_, err = io.Copy(w, manualFile)
		return err
	}

	data, err := io.ReadAll(manualFile)
222
223
224
225
226
227
228
229
230

231
232
233
234
235
236
237
222
223
224
225
226
227
228


229
230
231
232
233
234
235
236







-
-
+







	var buf bytes.Buffer
	if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil {
		return err
	}
	if _, err = m.WriteComputed(&buf); err != nil {
		return err
	}
	version := getVersion()
	if _, err = fmt.Fprintf(&buf, "\n%s", version); err != nil {
	if _, err = fmt.Fprintf(&buf, "\n%s", getVersion()); err != nil {
		return err
	}
	_, err = io.Copy(w, &buf)
	return err
}

//--- release
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
285
286
287
288
289
290
291

292
293


294
295

296
297

298

299
300

301

302
303
304
305
306
307
308

309
310
311
312
313
314
315
316







-
+

-
-
+
+
-


-
+
-


-
+
-







-
+







}

func createReleaseZip(zsName, zipName, fileName string) error {
	zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	defer zipFile.Close()
	defer func() { _ = zipFile.Close() }()
	zw := zip.NewWriter(zipFile)
	defer zw.Close()
	err = addFileToZip(zw, zsName, fileName)
	defer func() { _ = zw.Close() }()
	if err = addFileToZip(zw, zsName, fileName); err != nil {
	if err != nil {
		return err
	}
	err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt")
	if err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt"); err != nil {
	if err != nil {
		return err
	}
	err = addFileToZip(zw, "docs/readmezip.txt", "README.txt")
	return addFileToZip(zw, "docs/readmezip.txt", "README.txt")
	return err
}

func addFileToZip(zipFile *zip.Writer, filepath, filename string) error {
	zsFile, err := os.Open(filepath)
	if err != nil {
		return err
	}
	defer zsFile.Close()
	defer func() { _ = zsFile.Close() }()
	stat, err := zsFile.Stat()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(stat)
	if err != nil {
		return err
Changes to tools/testapi/testapi.go.
88
89
90
91
92
93
94
95
96
97





98
99
100
101
102
103
104
105
106

107
108
88
89
90
91
92
93
94



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

108
109
110







-
-
-
+
+
+
+
+








-
+



func stopZettelstore(i *zsInfo) error {
	conn, err := net.Dial("tcp", i.adminAddress)
	if err != nil {
		fmt.Println("Unable to stop Zettelstore")
		return err
	}
	io.WriteString(conn, "shutdown\n")
	conn.Close()
	err = i.cmd.Wait()
	_, err = io.WriteString(conn, "shutdown\n")
	_ = conn.Close()
	if err == nil {
		err = i.cmd.Wait()
	}
	return err
}

func addressInUse(address string) bool {
	conn, err := net.Dial("tcp", address)
	if err != nil {
		return false
	}
	conn.Close()
	_ = conn.Close()
	return true
}
Changes to tools/tools.go.
93
94
95
96
97
98
99



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







+
+
+







		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	if err := checkRevive(); err != nil {
		return err
	}
	if err := checkErrCheck(); err != nil {
		return err
	}
	if forRelease {
		if err := checkGoVulncheck(); err != nil {
			return err
		}
	}
	return checkFossilExtra()
164
165
166
167
168
169
170











171
172
173
174
175
176
177
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191







+
+
+
+
+
+
+
+
+
+
+







func checkRevive() error {
	out, err := ExecuteCommand(EnvGoVCS, "revive", "./...")
	if err != nil || out != "" {
		fmt.Fprintln(os.Stderr, "Some revive problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkErrCheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "errcheck", "./...")
	if err != nil || out != "" {
		fmt.Fprintln(os.Stderr, "Some errcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {
Changes to www/changes.wiki.
1
2
3
4








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




+
+
+
+
+
+
+
+







<title>Change Log</title>

<a id="0_22"></a>
<h2>Changes for Version 0.22.0 (pending)</h2>
  *  Remove zettel for Sx prelude. Luckily, it was never documented. So nobody
     used this zettel. Now the prelude is a constant string in Sx.
     (breaking)
  *  If authentication is enabled, allow to access Zettelstore from loopback
     device without logging in / get an access token.
     (major: api, webui)
  *  Move context link to zettel page.
     (minor: webui)

<a id="0_21"></a>
<h2>Changes for Version 0.21.0 (2025-04-17)</h2>
  *  Zettel identifier of Zettelstore Log, Zettelstore Memory, and Zettelstore
     Sx Engine changed. See manual for new identifier.
     (breaking)
  *  Sz encodings of links were simplified into one <code>LINK</code> symbol.