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-06-23
17:41
Very initial implementation of thread queries ... (Leaf check-in: 2be9d8ee3f user: stern tags: trunk)
13:07
Add zsx to dependency zettel ... (check-in: 5e978a78be 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
0.21.0
|
1
0.22.0-dev
Changes to cmd/cmd_run.go.
17
18
19
20
21
22
23

24
25
26
27
28
29
30
	"context"
	"flag"
	"net/http"

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

	"zettelstore.de/z/internal/auth"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter/api"
	"zettelstore.de/z/internal/web/adapter/webui"
	"zettelstore.de/z/internal/web/server"







>







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	"context"
	"flag"
	"net/http"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter/api"
	"zettelstore.de/z/internal/web/adapter/webui"
	"zettelstore.de/z/internal/web/server"
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
	kernel.Main.WaitForShutdown()
	return exitCode, err
}

func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) {
	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig)
	kern := kernel.Main
	webLog := kern.GetLogger(kernel.WebService)

	var getUser getUserImpl
	logAuth := kern.GetLogger(kernel.AuthService)
	logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser)
	ucGetUser := usecase.NewGetUser(authManager, boxManager)
	ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser)
	ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucGetReferences := usecase.NewGetReferences()
	ucQuery := usecase.NewQuery(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery)
	ucQuery.SetEvaluate(&ucEvaluate)
	ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery)
	ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery)
	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager)
	ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager)
	ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	a := api.New(
		webLog.Clone().Str("adapter", "api").Child(),
		webSrv, authManager, authManager, rtConfig, authPolicy)
	wui := webui.New(
		webLog.Clone().Str("adapter", "wui").Child(),
		webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate)

	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
	if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" {
		const assetPrefix = "/assets/"
		webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir))))
		webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir))







|


|
|

|
|
|











|
|
|
|



|


|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
	kernel.Main.WaitForShutdown()
	return exitCode, err
}

func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) {
	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig)
	kern := kernel.Main
	webLogger := kern.GetLogger(kernel.WebService)

	var getUser getUserImpl
	authLogger := kern.GetLogger(kernel.AuthService)
	ucLogger := kern.GetLogger(kernel.CoreService)
	ucGetUser := usecase.NewGetUser(authManager, boxManager)
	ucAuthenticate := usecase.NewAuthenticate(authLogger, authManager, &ucGetUser)
	ucIsAuth := usecase.NewIsAuthenticated(ucLogger, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(ucLogger, rtConfig, protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucGetReferences := usecase.NewGetReferences()
	ucQuery := usecase.NewQuery(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery)
	ucQuery.SetEvaluate(&ucEvaluate)
	ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery)
	ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery)
	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucDelete := usecase.NewDeleteZettel(ucLogger, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(ucLogger, protectedBoxManager)
	ucRefresh := usecase.NewRefresh(ucLogger, protectedBoxManager)
	ucReIndex := usecase.NewReIndex(ucLogger, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	a := api.New(
		webLogger.With("system", "WEBAPI"),
		webSrv, authManager, authManager, rtConfig, authPolicy)
	wui := webui.New(
		webLogger.With("system", "WEBUI"),
		webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate)

	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
	if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" {
		const assetPrefix = "/assets/"
		webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir))))
		webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir))
132
133
134
135
136
137
138
139
	if authManager.WithAuth() {
		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
	}
}

type getUserImpl struct{}

func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) }







|
133
134
135
136
137
138
139
140
	if authManager.WithAuth() {
		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
	}
}

type getUserImpl struct{}

func (*getUserImpl) GetCurrentUser(ctx context.Context) *meta.Meta { return user.GetCurrentUser(ctx) }
Changes to cmd/command.go.
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package cmd

import (
	"flag"

	"maps"
	"slices"

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

// Command stores information about commands / sub-commands.
type Command struct {
	Name       string              // command name as it appears on the command line
	Func       CommandFunc         // function that executes a command
	Simple     bool                // Operate in simple-mode







>


<
<







11
12
13
14
15
16
17
18
19
20


21
22
23
24
25
26
27
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package cmd

import (
	"flag"
	"log/slog"
	"maps"
	"slices"


)

// Command stores information about commands / sub-commands.
type Command struct {
	Name       string              // command name as it appears on the command line
	Func       CommandFunc         // function that executes a command
	Simple     bool                // Operate in simple-mode
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	if cmd.Name == "" || cmd.Func == nil {
		panic("Required command values missing")
	}
	if _, ok := commands[cmd.Name]; ok {
		panic("Command already registered: " + cmd.Name)
	}
	cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError)
	cmd.flags.String("l", logger.InfoLevel.String(), "log level specification")

	if cmd.SetFlags != nil {
		cmd.SetFlags(cmd.flags)
	}
	commands[cmd.Name] = cmd
}








|







47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
	if cmd.Name == "" || cmd.Func == nil {
		panic("Required command values missing")
	}
	if _, ok := commands[cmd.Name]; ok {
		panic("Command already registered: " + cmd.Name)
	}
	cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError)
	cmd.flags.String("l", slog.LevelInfo.String(), "log level specification")

	if cmd.SetFlags != nil {
		cmd.SetFlags(cmd.flags)
	}
	commands[cmd.Name] = cmd
}

Changes to cmd/main.go.
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Package cmd provides the commands to call Zettelstore from the command line.
package cmd

import (
	"crypto/sha256"
	"flag"
	"fmt"

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

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/auth/impl"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/compbox"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/web/server"
)

const strRunSimple = "run-simple"

func init() {
	RegisterCommand(Command{







>




















|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Package cmd provides the commands to call Zettelstore from the command line.
package cmd

import (
	"crypto/sha256"
	"flag"
	"fmt"
	"log/slog"
	"net"
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/auth/impl"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/compbox"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/web/server"
)

const strRunSimple = "run-simple"

func init() {
	RegisterCommand(Command{
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
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyInsecureHTML      = "insecure-html"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"


	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyReadOnly          = "read-only-mode"
	keyRuntimeProfiling  = "runtime-profiling"

	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

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







>
>





>








|
|







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
	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"
	keySxNesting         = "sx-max-nesting"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) bool {
	debugMode := cfg.GetBool(keyDebug)
	if debugMode && kernel.Main.GetKernelLogLevel() > slog.LevelDebug {
		kernel.Main.SetLogLevel(logging.LevelString(slog.LevelDebug))
	}
	if logLevel, found := cfg.Get(keyLogLevel); found {
		kernel.Main.SetLogLevel(string(logLevel))
	}
	err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode)
	err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
	if val, found := cfg.Get(keyAdminPort); found {
213
214
215
216
217
218
219


220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238



239
240
241
242
243
244
245
246

247
248
249
250
251
252
253
	}

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

	err = setConfigValue(
		err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))


	if val, found := cfg.Get(keyBaseURL); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val)
	}
	if val, found := cfg.Get(keyURLPrefix); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val)
	}
	err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))
	err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling))
	if val, found := cfg.Get(keyAssetDir); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val)
	}



	return err == nil
}

func setConfigValue(err error, subsys kernel.Service, key string, val any) error {
	if err == nil {
		err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val))
		if err != nil {
			kernel.Main.GetKernelLogger().Error().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration")

		}
	}
	return err
}

func executeCommand(name string, args ...string) int {
	command, ok := Get(name)







>
>



















>
>
>





|
<
|
>







217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253

254
255
256
257
258
259
260
261
262
	}

	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))
	err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))
	err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling))
	if val, found := cfg.Get(keyAssetDir); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val)
	}
	if val, found := cfg.Get(keySxNesting); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebSxMaxNesting, val)
	}
	return err == nil
}

func setConfigValue(err error, subsys kernel.Service, key string, val any) error {
	if err == nil {
		if err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)); err != nil {

			kernel.Main.GetKernelLogger().Error("Unable to set configuration",
				"key", key, "value", val, "err", err)
		}
	}
	return err
}

func executeCommand(name string, args ...string) int {
	command, ok := Get(name)
293
294
295
296
297
298
299
300



301
302
303
304
305
306
307
		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")



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







|
>
>
>







302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
		func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error {
			setupRouting(srv, plMgr, authMgr, rtConfig)
			return nil
		},
	)

	if command.Simple {
		if err := kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true"); err != nil {
			kern.GetKernelLogger().Error("unable to set simple-mode", "err", err)
			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
	fullVersion := info.revision
	if info.dirty {
		fullVersion += "-dirty"
	}
	kernel.Main.Setup(progName, fullVersion, info.time)
	flag.Parse()
	if *cpuprofile != "" || *memprofile != "" {

		if *cpuprofile != "" {
			kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile)
		} else {
			kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile)
		}





		defer kernel.Main.StopProfiling()



	}
	args := flag.Args()
	if len(args) == 0 {
		return runSimple()
	}
	return executeCommand(args[0], args[1:]...)
}







>

|

|

>
>
>
>
>
|
>
>
>







343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
	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 != "" {
			err = kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile)
		} else {
			err = kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile)
		}
		if err != nil {
			kernel.Main.GetKernelLogger().Error("start profiling", "err", err)
			return 1
		}
		defer func() {
			if err = kernel.Main.StopProfiling(); err != nil {
				kernel.Main.GetKernelLogger().Error("stop profiling", "err", err)
			}
		}()
	}
	args := flag.Args()
	if len(args) == 0 {
		return runSimple()
	}
	return executeCommand(args[0], args[1:]...)
}
Changes to docs/manual/00001002000000.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001002000000
title: Design goals for the Zettelstore
role: manual
tags: #design #goal #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102191434

Zettelstore supports the following design goals:

; Longevity of stored notes / zettel
: Every zettel you create should be readable without the help of any tool, even without Zettelstore.
: It should not hard to write other software that works with your zettel.
: Normal zettel should be stored in a single file.
  If this is not possible: at most in two files: one for the metadata, one for the content.
  The only exceptions are [[predefined zettel|00001005090000]] stored in the Zettelstore executable.
: There is no additional database.
; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.






|





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001002000000
title: Design goals for the Zettelstore
role: manual
tags: #design #goal #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250602181324

Zettelstore supports the following design goals:

; Longevity of stored notes / zettel
: Every zettel you create should be readable without the help of any tool, even without Zettelstore.
: It should not be hard to write other software that works with your zettel.
: Normal zettel should be stored in a single file.
  If this is not possible: at most in two files: one for the metadata, one for the content.
  The only exceptions are [[predefined zettel|00001005090000]] stored in the Zettelstore executable.
: There is no additional database.
; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.
35
36
37
38
39
40
41
42
43
; Simple service
: The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them.
: External software can be written to deeply analyze your zettel and the structures they form.
; Security by default
: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks.
: If you know what you are doing, Zettelstore allows you to relax some security-related preferences.
  However, even in this case, the more secure way is chosen.
: The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed.
: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software.







|

35
36
37
38
39
40
41
42
43
; Simple service
: The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them.
: External software can be written to deeply analyze your zettel and the structures they form.
; Security by default
: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks.
: If you know what you are doing, Zettelstore allows you to relax some security-related preferences.
  However, even in this case, the more secure way is chosen.
: Zettelstore features a minimal design and relies on external software only when absolutely necessary.
: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software.
Changes to docs/manual/00001004010000.zettel.
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

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.







|







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

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

  Default: ""info"".

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

  When you are familiar with operating the Zettelstore, you might set the level to ""error"" to receive fewer noisy messages from it.












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

  Default: 16777216 (16 MiB). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.







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







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.
127
128
129
130
131
132
133






134
135
136
137
138
139
140
  Default: ""false"", but it is set to ""true"" if [[''debug-mode''|#debug-mode]] is enabled.
  In this case, it cannot be disabled.
; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be altered by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  This value is only needed to be set if [[authentication is enabled|00001010040100]] by setting the key [[''owner''|#owner]] to some user identification value.






; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]].
  Default: ""10"".








>
>
>
>
>
>







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
  Default: ""false"", but it is set to ""true"" if [[''debug-mode''|#debug-mode]] is enabled.
  In this case, it cannot be disabled.
; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be altered by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  This value is only needed to be set if [[authentication is enabled|00001010040100]] by setting the key [[''owner''|#owner]] to some user identification value.
; [!sx-max-nesting|''sx-max-nesting'']
: Limits nesting of template evaluation to the given value.

  This is used to prevent the application from crashing when an error occurs in a template zettel of the Web User Interface.

  Default: ""32768""
; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]].
  Default: ""10"".

Changes to docs/manual/00001004011400.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102180416

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

The following parameters are supported:

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

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

To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box.
The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual.
The following values are supported:

; simple






|













|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250602181524

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

The following parameters are supported:

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

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

To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box.
The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual.
The following values are supported:

; simple
Changes to docs/manual/00001006020400.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102205707

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

=== No authentication






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250602181627

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

=== No authentication
22
23
24
25
26
27
28
29
30
31
32
33
34

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

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

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







|





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

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

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

If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended), no user is allowed to modify the zettel through the [[web user interface|00001014000000]].
However, if the zettel is accessible as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor.
Typically the owner of a Zettelstore has such access.
Changes to docs/manual/00001007720300.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
id: 00001007720300
title: Query: Context Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707204706
modified: 20250202172633

A context directive calculates the __context__ of a list of zettel identifier.
It starts with the keyword ''CONTEXT''.

Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters.
These are:
* ''FULL'': additionally search for zettel with the same tags,
* ''BACKWARD'': search for context only though backward links,
* ''FORWARD'': search for context only through forward links,
* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17),
* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200).
* ''MIN'': one or more space characters, and a positive integer: set the minimum number of context zettel (default: 0).
  Takes precedence over ''COST'' and ''MAX''.

If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links.

The cost of a context zettel is calculated iteratively:
* Each of the specified zettel hast a cost of one.

* A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus 0.1.
* A zettel found as a single sequel zettel or single prequel zettel has the cost of the originating zettel, plus 1.0.
* A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus seven.
* A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus two.
* A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the four choices above and multiplied with roughly a linear-logarithmic value based on the size of the set.
* A zettel with the same tag, has the cost of the originating zettel, plus a linear-logarithmic number based on the number of zettel with this tag.
  If a zettel belongs to more than one tag compared with the current zettel, there is a discount of 90% per additional tag.
  This only applies if the ''FULL'' directive was specified.

The maximum cost is only checked for all zettel that are not directly reachable from the initial, specified list of zettel.
This ensures that initial zettel that have only a highly used tag, will also produce some context zettel.

Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel.
It also penalties zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list.

This directive may be specified only once as a query directive.
A second occurence of ''CONTEXT'' is interpreted as a [[search expression|00001007701000]].
In most cases it is easier to adjust the maximum cost than to perform another context search, which is relatively expensive in terms of retrieving effort.






|

















|
>
|
|
|
|
|
|
|
|










1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
id: 00001007720300
title: Query: Context Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707204706
modified: 20250623144037

A context directive calculates the __context__ of a list of zettel identifier.
It starts with the keyword ''CONTEXT''.

Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters.
These are:
* ''FULL'': additionally search for zettel with the same tags,
* ''BACKWARD'': search for context only though backward links,
* ''FORWARD'': search for context only through forward links,
* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17),
* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200).
* ''MIN'': one or more space characters, and a positive integer: set the minimum number of context zettel (default: 0).
  Takes precedence over ''COST'' and ''MAX''.

If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links.

The cost of a context zettel is calculated iteratively:
* Each of the specified zettel has a cost of 1.0.
* Every zettel directly referenced by a specified zettel has a maximum cost of 4.0.
* A zettel found as a single folge zettel or single precursor zettel inherits the cost of the originating zettel, plus 0.2.
* A zettel found as a single sequel zettel or single prequel zettel inherits the cost of the originating zettel, plus 1.0.
* A zettel found as a single successor zettel or single predecessor zettel inherits the cost of the originating zettel, plus 7.0.
* A zettel discovered via another type of link, without being part of a [[set of zettel identifiers|00001006032500]], inherits the cost of the originating zettel, plus 2.0.
* A zettel that is part of a set of zettel identifiers, inherits the cost of the originating zettel, plus one of the four costs above, multiplied by a value that grows roughly in a linear-logarithmic fashion based on the size of the set.
* A zettel sharing the same tag inherits the cost of the originating zettel, plus a linear-logarithmic value based on the number of zettel sharing that tag.
  If a zettel shares more than one tag with the originating zettel, a 90% discount is applied per additional tag.
  This rules applies only if the ''FULL'' directive has been specified.

The maximum cost is only checked for all zettel that are not directly reachable from the initial, specified list of zettel.
This ensures that initial zettel that have only a highly used tag, will also produce some context zettel.

Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel.
It also penalties zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list.

This directive may be specified only once as a query directive.
A second occurence of ''CONTEXT'' is interpreted as a [[search expression|00001007701000]].
In most cases it is easier to adjust the maximum cost than to perform another context search, which is relatively expensive in terms of retrieving effort.
Changes to docs/manual/00001010040100.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
id: 00001010040100
title: Enable authentication
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220419192817


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

Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to some random string data (minimum length is 16 bytes) to secure the data exchanged with a client system.






|
>






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: 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 docs/manual/00001010090100.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250415165219

Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key.
Technically, this is not trivial.
Any client who wants to communicate with your Zettelstore must trust the public encryption key.
Otherwise the client cannot be sure that it is communication with your Zettelstore.
This problem is solved in part with [[Let's Encrypt|https://letsencrypt.org/]],
""a free, automated, and open certificate authority (CA), run for the public’s benefit.
It is a service provided by the [[Internet Security Research Group|https://www.abetterinternet.org/]]"".







|





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250602182154

Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publishes a public encryption key that only works with a corresponding private decryption key.
Technically, this is not trivial.
Any client who wants to communicate with your Zettelstore must trust the public encryption key.
Otherwise the client cannot be sure that it is communication with your Zettelstore.
This problem is solved in part with [[Let's Encrypt|https://letsencrypt.org/]],
""a free, automated, and open certificate authority (CA), run for the public’s benefit.
It is a service provided by the [[Internet Security Research Group|https://www.abetterinternet.org/]]"".

60
61
62
63
64
65
66
67
68
69
70
71
    root /var/www/html
  }
  route /manual/* {
    reverse_proxy localhost:23123
  }
}
```
This will forwards requests with the prefix ""/manual/"" to the running Zettelstore.
All other requests will be handled by Caddy itself.

In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"".
This is to allow Zettelstore to ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response.







|




60
61
62
63
64
65
66
67
68
69
70
71
    root /var/www/html
  }
  route /manual/* {
    reverse_proxy localhost:23123
  }
}
```
This will forward requests with the prefix ""/manual/"" to the running Zettelstore.
All other requests will be handled by Caddy itself.

In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"".
This is to allow Zettelstore to ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response.
Changes to docs/manual/00001012051400.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20250224120055
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally specify some actions.

A [[query|00001007700000]] consists of an optional [[search expression|00001007700000#search-expression]] and an optional [[list of actions|00001007700000#action-list]] (described below).
If no search expression is provided, all zettel are selected.
Similarly, if no valid action is specified, or the action list is empty, the list of all selected zettel metadata is returned.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20250602182055
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally specify some actions.

A [[query|00001007700000]] consists of an optional [[search expression|00001007700000#search-expression]] and an optional [[list of actions|00001007700000#action-list]] (described below).
If no search expression is provided, all zettel are selected.
Similarly, if no valid action is specified, or the action list is empty, the list of all selected zettel metadata is returned.
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

An implicit precondition is that the zettel must contain the given metadata key.
For a metadata key like [[''title''|00001006020000#title]], which have a default value, this precondition should always be true.
But the situation is different for a key like [[''url''|00001006020000#url]].
Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list.


As an example for a query action, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|role''.

```sh
# curl 'http://127.0.0.1:23123/z?q=|role'
configuration	00001000000100 00000000090002 00000000090000 00000000040001 00000000025001 00000000020001 00000000000100 00000000000092 00000000000090 00000000000006 00000000000005 00000000000004 00000000000001
manual	00001018000000 00001017000000 00001014000000 00001012921200 00001012921000 00001012920800 00001012920588 00001012920584 00001012920582 00001012920522 00001012920519 00001012920516 00001012920513 00001012920510 00001012920503 00001012920500 00001012920000 00001012080500 00001012080200 00001012080100 00001012070500 00001012054600 00001012054400 00001012054200 00001012054000 00001012053900 00001012053800 00001012053600 00001012053500 00001012053400 00001012053300 00001012053200 00001012051400 00001012051200 00001012050600 00001012050400 00001012050200 00001012000000 00001010090100 00001010070600 00001010070400 00001010070300 00001010070200 00001010040700 00001010040400 00001010040200 00001010040100 00001010000000 00001008050000 00001008010500 00001008010000 00001008000000 00001007990000 00001007906000 00001007903000 00001007900000 00001007800000 00001007790000 00001007780000 00001007706000 00001007705000 00001007702000 00001007700000 00001007050200 00001007050100 00001007050000 00001007040350 00001007040340 00001007040330 00001007040324 00001007040322 00001007040320 00001007040310 00001007040300 00001007040200 00001007040100 00001007040000 00001007031400 00001007031300 00001007031200 00001007031140 00001007031110 00001007031100 00001007031000 00001007030900 00001007030800 00001007030700 00001007030600 00001007030500 00001007030400 00001007030300 00001007030200 00001007030100 00001007030000 00001007020000 00001007010000 00001007000000 00001006055000 00001006050000 00001006036500 00001006036000 00001006035500 00001006035000 00001006034500 00001006034000 00001006033500 00001006033000 00001006032500 00001006032000 00001006031500 00001006031000 00001006030500 00001006030000 00001006020400 00001006020100 00001006020000 00001006010000 00001006000000 00001005090000 00001005000000 00001004101000 00001004100000 00001004059900 00001004059700 00001004051400 00001004051200 00001004051100 00001004051000 00001004050400 00001004050200 00001004050000 00001004020200 00001004020000 00001004011600 00001004011400 00001004011200 00001004010000 00001004000000 00001003600000 00001003315000 00001003310000 00001003305000 00001003300000 00001003000000 00001002000000 00001001000000 00001000000000
zettel	00010000000000 00000000090001
```

The result is a text file.
The first word, separated by a horizontal tab (U+0009) contains the role name.
The rest of the line consists of zettel identifier, where the corresponding zettel have this role.
Zettel identifier are separated by a space character (U+0020).

Please note that the list is **not** sorted by the role name, so the same request might result in a different order.
If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore.

Of course, this list can also be returned as a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|role&enc=data'
(aggregate "role" (query "| role") (human "| role") ("zettel" 10000000000 90001) ("configuration" 6 100 1000000100 20001 90 25001 92 4 40001 1 90000 5 90002) ("manual" 1008050000 1007031110 1008000000 1012920513 1005000000 1012931800 1010040700 1012931000 1012053600 1006050000 1012050200 1012000000 1012070500 1012920522 1006032500 1006020100 1007906000 1007030300 1012051400 1007040350 1007040324 1007706000 1012931900 1006030500 1004050200 1012054400 1007700000 1004050000 1006020000 1007030400 1012080100 1012920510 1007790000 1010070400 1005090000 1004011400 1006033000 1012930500 1001000000 1007010000 1006020400 1007040300 1010070300 1008010000 1003305000 1006030000 1006034000 1012054200 1012080200 1004010000 1003300000 1006032000 1003310000 1004059700 1007031000 1003600000 1004000000 1007030700 1007000000 1006055000 1007050200 1006036000 1012050600 1006000000 1012053900 1012920500 1004050400 1007031100 1007040340 1007020000 1017000000 1012053200 1007030600 1007040320 1003315000 1012054000 1014000000 1007030800 1010000000 1007903000 1010070200 1004051200 1007040330 1004051100 1004051000 1007050100 1012080500 1012053400 1006035500 1012054600 1004100000 1010040200 1012920000 1012920525 1004051400 1006031500 1012921200 1008010500 1012921000 1018000000 1012051200 1010040100 1012931200 1012920516 1007040310 1007780000 1007030200 1004101000 1012920800 1007030100 1007040200 1012053500 1007040000 1007040322 1007031300 1007031140 1012931600 1012931400 1004059900 1003000000 1006036500 1004020200 1010040400 1006033500 1000000000 1012053300 1007990000 1010090100 1007900000 1007030500 1004011600 1012930000 1007030900 1004020000 1007030000 1010070600 1007040100 1007800000 1012050400 1006010000 1007705000 1007702000 1007050000 1002000000 1007031200 1006035000 1006031000 1006034500 1004011200 1007031400 1012920519))
```

The data object starts with the symbol ''aggregate'' to signal a different format compared to ''meta-list'' above.
Then a string follows, which specifies the key on which the aggregate was performed.
''query'' and ''human'' have the same meaning as above.
Then comes the result list of aggregates.
Each aggregate starts with a string of the aggregate value, in this case the role value, followed by a list of zettel identifier, denoting zettel which have the given role value.

Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|tags''.
If successful, the output is a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=data'
(aggregate "tags" (query "| tags") (human "| tags") ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ...
```








|











|

















|







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

An implicit precondition is that the zettel must contain the given metadata key.
For a metadata key like [[''title''|00001006020000#title]], which have a default value, this precondition should always be true.
But the situation is different for a key like [[''url''|00001006020000#url]].
Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list.


As an example for a query action, to list all roles used in the Zettelstore, send an HTTP GET request to the endpoint ''/z?q=|role''.

```sh
# curl 'http://127.0.0.1:23123/z?q=|role'
configuration	00001000000100 00000000090002 00000000090000 00000000040001 00000000025001 00000000020001 00000000000100 00000000000092 00000000000090 00000000000006 00000000000005 00000000000004 00000000000001
manual	00001018000000 00001017000000 00001014000000 00001012921200 00001012921000 00001012920800 00001012920588 00001012920584 00001012920582 00001012920522 00001012920519 00001012920516 00001012920513 00001012920510 00001012920503 00001012920500 00001012920000 00001012080500 00001012080200 00001012080100 00001012070500 00001012054600 00001012054400 00001012054200 00001012054000 00001012053900 00001012053800 00001012053600 00001012053500 00001012053400 00001012053300 00001012053200 00001012051400 00001012051200 00001012050600 00001012050400 00001012050200 00001012000000 00001010090100 00001010070600 00001010070400 00001010070300 00001010070200 00001010040700 00001010040400 00001010040200 00001010040100 00001010000000 00001008050000 00001008010500 00001008010000 00001008000000 00001007990000 00001007906000 00001007903000 00001007900000 00001007800000 00001007790000 00001007780000 00001007706000 00001007705000 00001007702000 00001007700000 00001007050200 00001007050100 00001007050000 00001007040350 00001007040340 00001007040330 00001007040324 00001007040322 00001007040320 00001007040310 00001007040300 00001007040200 00001007040100 00001007040000 00001007031400 00001007031300 00001007031200 00001007031140 00001007031110 00001007031100 00001007031000 00001007030900 00001007030800 00001007030700 00001007030600 00001007030500 00001007030400 00001007030300 00001007030200 00001007030100 00001007030000 00001007020000 00001007010000 00001007000000 00001006055000 00001006050000 00001006036500 00001006036000 00001006035500 00001006035000 00001006034500 00001006034000 00001006033500 00001006033000 00001006032500 00001006032000 00001006031500 00001006031000 00001006030500 00001006030000 00001006020400 00001006020100 00001006020000 00001006010000 00001006000000 00001005090000 00001005000000 00001004101000 00001004100000 00001004059900 00001004059700 00001004051400 00001004051200 00001004051100 00001004051000 00001004050400 00001004050200 00001004050000 00001004020200 00001004020000 00001004011600 00001004011400 00001004011200 00001004010000 00001004000000 00001003600000 00001003315000 00001003310000 00001003305000 00001003300000 00001003000000 00001002000000 00001001000000 00001000000000
zettel	00010000000000 00000000090001
```

The result is a text file.
The first word, separated by a horizontal tab (U+0009) contains the role name.
The rest of the line consists of zettel identifier, where the corresponding zettel have this role.
Zettel identifiers are separated by a space character (U+0020).

Please note that the list is **not** sorted by the role name, so the same request might result in a different order.
If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore.

Of course, this list can also be returned as a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|role&enc=data'
(aggregate "role" (query "| role") (human "| role") ("zettel" 10000000000 90001) ("configuration" 6 100 1000000100 20001 90 25001 92 4 40001 1 90000 5 90002) ("manual" 1008050000 1007031110 1008000000 1012920513 1005000000 1012931800 1010040700 1012931000 1012053600 1006050000 1012050200 1012000000 1012070500 1012920522 1006032500 1006020100 1007906000 1007030300 1012051400 1007040350 1007040324 1007706000 1012931900 1006030500 1004050200 1012054400 1007700000 1004050000 1006020000 1007030400 1012080100 1012920510 1007790000 1010070400 1005090000 1004011400 1006033000 1012930500 1001000000 1007010000 1006020400 1007040300 1010070300 1008010000 1003305000 1006030000 1006034000 1012054200 1012080200 1004010000 1003300000 1006032000 1003310000 1004059700 1007031000 1003600000 1004000000 1007030700 1007000000 1006055000 1007050200 1006036000 1012050600 1006000000 1012053900 1012920500 1004050400 1007031100 1007040340 1007020000 1017000000 1012053200 1007030600 1007040320 1003315000 1012054000 1014000000 1007030800 1010000000 1007903000 1010070200 1004051200 1007040330 1004051100 1004051000 1007050100 1012080500 1012053400 1006035500 1012054600 1004100000 1010040200 1012920000 1012920525 1004051400 1006031500 1012921200 1008010500 1012921000 1018000000 1012051200 1010040100 1012931200 1012920516 1007040310 1007780000 1007030200 1004101000 1012920800 1007030100 1007040200 1012053500 1007040000 1007040322 1007031300 1007031140 1012931600 1012931400 1004059900 1003000000 1006036500 1004020200 1010040400 1006033500 1000000000 1012053300 1007990000 1010090100 1007900000 1007030500 1004011600 1012930000 1007030900 1004020000 1007030000 1010070600 1007040100 1007800000 1012050400 1006010000 1007705000 1007702000 1007050000 1002000000 1007031200 1006035000 1006031000 1006034500 1004011200 1007031400 1012920519))
```

The data object starts with the symbol ''aggregate'' to signal a different format compared to ''meta-list'' above.
Then a string follows, which specifies the key on which the aggregate was performed.
''query'' and ''human'' have the same meaning as above.
Then comes the result list of aggregates.
Each aggregate starts with a string of the aggregate value, in this case the role value, followed by a list of zettel identifier, denoting zettel which have the given role value.

Similarly, to list all tags used in the Zettelstore, send an HTTP GET request to the endpoint ''/z?q=|tags''.
If successful, the output is a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=data'
(aggregate "tags" (query "| tags") (human "| tags") ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ...
```

108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'' (aggregate)
: Performs a HTTP redirect to the first selected zettel, using HTTP status code 302.
  The zettel identifier is in the body.
; ''REINDEX'' (aggregate)
: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call.
  It is not really an aggregate, since it is used only for its side effect.
  It is allowed to specify another aggregate.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.







|







108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'' (aggregate)
: Performs an HTTP redirect to the first selected zettel, using HTTP status code 302.
  The zettel identifier is in the body.
; ''REINDEX'' (aggregate)
: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call.
  It is not really an aggregate, since it is used only for its side effect.
  It is allowed to specify another aggregate.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
Changes to docs/manual/00001012921200.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220201173115
modified: 20240711183931

Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has.
''N'' is an integer number between 0 and 62.[^Not all values in this range are used.]

The value ""0"" signals that something went wrong internally while determining the access rights.

A value of ""1"" says, that the current user has no access right for the given zettel.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220201173115
modified: 20250602181802

Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has.
''N'' is an integer number between 0 and 62.[^Not all values in this range are used.]

The value ""0"" signals that something went wrong internally while determining the access rights.

A value of ""1"" says, that the current user has no access right for the given zettel.
36
37
38
39
40
41
42
43
44
45
46
# The next right is the right to update a zettel (16 > 10, but 8 < 10).
  The new value of the rights value is now 2 (10-8).
# The last right is the right to create a new zettel.
  The rights value is now zero, the algorithm ends.

In practice, not every rights value will occur.
A Zettelstore in [[read-only mode|00001010000000#read-only]] will always return the value 4.
Similar, a Zettelstore that you started with a [[double-click|00001003000000]] will return either the value ""6"" (reading and updating) or the value ""62"" (all operations are allowed).

If you have added an additional [[user|00001010040200]] to your Zettelstore, this might change.
The access rights are calculated depending on [[enabled authentication|00001010040100]], on the [[user role|00001010070300]] of the current user, on [[visibility rules|00001010070200]] for a given zettel and on the [[read-only status|00001006020400]] for the zettel.







|



36
37
38
39
40
41
42
43
44
45
46
# The next right is the right to update a zettel (16 > 10, but 8 < 10).
  The new value of the rights value is now 2 (10-8).
# The last right is the right to create a new zettel.
  The rights value is now zero, the algorithm ends.

In practice, not every rights value will occur.
A Zettelstore in [[read-only mode|00001010000000#read-only]] will always return the value 4.
Similarly, a Zettelstore that you started with a [[double-click|00001003000000]] will return either the value ""6"" (read and update) or the value ""62"" (all operations are allowed).

If you have added an additional [[user|00001010040200]] to your Zettelstore, this might change.
The access rights are calculated depending on [[enabled authentication|00001010040100]], on the [[user role|00001010070300]] of the current user, on [[visibility rules|00001010070200]] for a given zettel and on the [[read-only status|00001006020400]] for the zettel.
Changes to go.mod.
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
	t73f.de/r/zsx v0.0.0-20250415162540-fc13b286b6ce
)

require golang.org/x/sys v0.32.0 // indirect






|
|
|
|
|
|
|
|
|
|


|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module zettelstore.de/z

go 1.24

require (
	github.com/fsnotify/fsnotify v1.9.0
	github.com/yuin/goldmark v1.7.12
	golang.org/x/crypto v0.39.0
	golang.org/x/term v0.32.0
	golang.org/x/text v0.26.0
	t73f.de/r/sx v0.0.0-20250620141036-553aa22c59dc
	t73f.de/r/sxwebs v0.0.0-20250621125212-c25706b6e4b3
	t73f.de/r/webs v0.0.0-20250604132257-c12dbd1f7046
	t73f.de/r/zero v0.0.0-20250604143210-ce1230735c4c
	t73f.de/r/zsc v0.0.0-20250623172803-70d15bf13ea8
	t73f.de/r/zsx v0.0.0-20250526093914-c34f0bae8fd2
)

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


|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
t73f.de/r/sx v0.0.0-20250620141036-553aa22c59dc h1:5s02C7lwQKjOXyY4ghR6oLo+0SagSBiEiC26ju3VG40=
t73f.de/r/sx v0.0.0-20250620141036-553aa22c59dc/go.mod h1:Ow4Btc5PCykSct4TrS1kbEB386msl/tmfmLSsEz6OAw=
t73f.de/r/sxwebs v0.0.0-20250621125212-c25706b6e4b3 h1:+tqWPX3z5BgsRZJDpMtReHmGUioUFP+LsPpXieZ2ZsY=
t73f.de/r/sxwebs v0.0.0-20250621125212-c25706b6e4b3/go.mod h1:zZBXrGeTfUqElkSMJhGUCuDDWNOUaZE0EH3IZwkW+RA=
t73f.de/r/webs v0.0.0-20250604132257-c12dbd1f7046 h1:BZWNT/wYlX5sHmEtClRG0rHzZnoh8J35NcRnTvXlqy0=
t73f.de/r/webs v0.0.0-20250604132257-c12dbd1f7046/go.mod h1:EVohQwCAlRK0kuVBEw5Gw+S44vj+6f6NU8eNJdAIK6s=
t73f.de/r/zero v0.0.0-20250604143210-ce1230735c4c h1:Zy7GaPv/uVSjKQY7t2c0OOIdSue36x+/0sXt+xoxlpQ=
t73f.de/r/zero v0.0.0-20250604143210-ce1230735c4c/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA=
t73f.de/r/zsc v0.0.0-20250623172803-70d15bf13ea8 h1:c7NowYXYqIRDbq5DWAjSjQKY8Uu3HSYzmC6l8V+Be9c=
t73f.de/r/zsc v0.0.0-20250623172803-70d15bf13ea8/go.mod h1:mxIDqZJD02ZD+pspYPa/VHdlMmUE+DBzE5J5dp+Vb/I=
t73f.de/r/zsx v0.0.0-20250526093914-c34f0bae8fd2 h1:GWLCd3n8mN6AGhiv8O7bhdjK0BqXQS5EExRlBdx3OPU=
t73f.de/r/zsx v0.0.0-20250526093914-c34f0bae8fd2/go.mod h1:IQdyC9JP1i6RK55+LJVGjP3hSA9H766yCyUt1AkOU9c=
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
	kernel.CoreGoArch,
	kernel.CoreVersion,
}

func calcSecret(extSecret string) []byte {
	h := fnv.New128()
	if extSecret != "" {
		io.WriteString(h, extSecret)
	}
	for _, key := range configKeys {
		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 }








|


|







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)
	}
	for _, key := range configKeys {
		_, _ = 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/auth/policy/box.go.
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
	"context"

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

	"zettelstore.de/z/internal/auth"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/web/server"
	"zettelstore.de/z/internal/zettel"
)

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







>



<







17
18
19
20
21
22
23
24
25
26
27

28
29
30
31
32
33
34
	"context"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/query"

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

// BoxWithPolicy wraps the given box inside a policy box.
func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) {
	pol := newPolicy(manager, authConfig)
	return newBox(box, pol), pol
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
}

func (pp *polBox) CanCreateZettel(ctx context.Context) bool {
	return pp.box.CanCreateZettel(ctx)
}

func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	user := server.GetUser(ctx)
	if pp.policy.CanCreate(user, zettel.Meta) {
		return pp.box.CreateZettel(ctx, zettel)
	}
	return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid)
}

func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return zettel.Zettel{}, err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanRead(user, z.Meta) {
		return z, nil
	}
	return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
}

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

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

func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	m, err := pp.box.GetMeta(ctx, zid)
	if err != nil {
		return nil, err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanRead(user, m) {
		return m, nil
	}
	return nil, box.NewErrNotAllowed("GetMeta", user, zid)
}

func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) {
	user := server.GetUser(ctx)
	canRead := pp.policy.CanRead
	q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
	return pp.box.SelectMeta(ctx, metaSeq, q)
}

func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool {
	return pp.box.CanUpdateZettel(ctx, zettel)
}

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







|











|











|







|







|











|







53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
}

func (pp *polBox) CanCreateZettel(ctx context.Context) bool {
	return pp.box.CanCreateZettel(ctx)
}

func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	user := user.GetCurrentUser(ctx)
	if pp.policy.CanCreate(user, zettel.Meta) {
		return pp.box.CreateZettel(ctx, zettel)
	}
	return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid)
}

func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return zettel.Zettel{}, err
	}
	user := user.GetCurrentUser(ctx)
	if pp.policy.CanRead(user, z.Meta) {
		return z, nil
	}
	return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
}

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

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

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

func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) {
	user := user.GetCurrentUser(ctx)
	canRead := pp.policy.CanRead
	q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
	return pp.box.SelectMeta(ctx, metaSeq, q)
}

func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool {
	return pp.box.CanUpdateZettel(ctx, zettel)
}

func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	zid := zettel.Meta.Zid
	user := user.GetCurrentUser(ctx)
	if !zid.IsValid() {
		return box.ErrInvalidZid{Zid: zid.String()}
	}
	// Write existing zettel
	oldZettel, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return err
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
}

func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanDelete(user, z.Meta) {
		return pp.box.DeleteZettel(ctx, zid)
	}
	return box.NewErrNotAllowed("Delete", user, zid)
}

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







|







|






|






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
}

func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return err
	}
	user := user.GetCurrentUser(ctx)
	if pp.policy.CanDelete(user, z.Meta) {
		return pp.box.DeleteZettel(ctx, zid)
	}
	return box.NewErrNotAllowed("Delete", user, zid)
}

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














































































































































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

// Package user provides services for working with user data.
package user

import (
	"context"
	"time"

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

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

// AuthData stores all relevant authentication data for a context.
type AuthData struct {
	User    *meta.Meta
	Token   []byte
	Now     time.Time
	Issued  time.Time
	Expires time.Time
}

// GetAuthData returns the full authentication data from the context.
func GetAuthData(ctx context.Context) *AuthData {
	if ctx != nil {
		if data, ok := ctx.Value(ctxKeyTypeSession{}).(*AuthData); ok {
			return data
		}
	}
	return nil
}

// GetCurrentUser returns the metadata of the current user, or nil if there is no one.
func GetCurrentUser(ctx context.Context) *meta.Meta {
	if data := GetAuthData(ctx); data != nil {
		return data.User
	}
	return nil
}

// ctxKeyTypeSession is just an additional type to make context value retrieval unambiguous.
type ctxKeyTypeSession struct{}

// UpdateContext enriches the given context with some data of the current user.
func UpdateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context {
	if data == nil {
		return context.WithValue(ctx, ctxKeyTypeSession{}, &AuthData{User: user})
	}
	return context.WithValue(
		ctx,
		ctxKeyTypeSession{},
		&AuthData{
			User:    user,
			Token:   data.Token,
			Now:     data.Now,
			Issued:  data.Issued,
			Expires: data.Expires,
		})
}
Changes to internal/box/box.go.
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
	Enrich(ctx context.Context, m *meta.Meta, boxNumber int)
}

// NoEnrichContext will signal an enricher that nothing has to be done.
// This is useful for an Indexer, but also for some box.Box calls, when
// just the plain metadata is needed.
func NoEnrichContext(ctx context.Context) context.Context {
	return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
}

type ctxNoEnrichType struct{}

var ctxNoEnrichKey ctxNoEnrichType

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

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







|




<
<


|







237
238
239
240
241
242
243
244
245
246
247
248


249
250
251
252
253
254
255
256
257
258
	Enrich(ctx context.Context, m *meta.Meta, boxNumber int)
}

// NoEnrichContext will signal an enricher that nothing has to be done.
// This is useful for an Indexer, but also for some box.Box calls, when
// just the plain metadata is needed.
func NoEnrichContext(ctx context.Context) context.Context {
	return context.WithValue(ctx, ctxNoEnrichType{}, ctx)
}

type ctxNoEnrichType struct{}



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

// NoEnrichQuery provides a context that signals not to enrich, if the query does not need this.
func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context {
	if q.EnrichNeeded() {
		return ctx
Changes to internal/box/compbox/compbox.go.
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//-----------------------------------------------------------------------------

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

import (
	"context"

	"net/url"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

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

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

var myConfig *meta.Meta
var myZettel = map[id.Zid]struct {
	meta    func(id.Zid) *meta.Meta







>








|













|







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

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

import (
	"context"
	"log/slog"
	"net/url"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

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

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

var myConfig *meta.Meta
var myZettel = map[id.Zid]struct {
	meta    func(id.Zid) *meta.Meta
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
	id.ZidParser:               {genParserM, genParserC},
	id.ZidStartupConfiguration: {genConfigZettelM, genConfigZettelC},
}

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

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

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

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

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

func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid")
	for zid, gen := range myZettel {
		if !constraint(zid) {
			continue
		}
		if genMeta := gen.meta; genMeta != nil {
			if genMeta(zid) != nil {
				handle(zid)
			}
		}
	}
	return nil
}

func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta")
	for zid, gen := range myZettel {
		if !constraint(zid) {
			continue
		}
		if genMeta := gen.meta; genMeta != nil {
			if m := genMeta(zid); m != nil {
				updateMeta(m)







|
<















|





|




|









|














|







67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
	id.ZidParser:               {genParserM, genParserC},
	id.ZidStartupConfiguration: {genConfigZettelM, genConfigZettelC},
}

// Get returns the one program box.
func getCompBox(boxNumber int, mf box.Enricher) *compBox {
	return &compBox{
		logger:   kernel.Main.GetLogger(kernel.BoxService).With("box", "comp", "boxnum", boxNumber),

		number:   boxNumber,
		enricher: mf,
	}
}

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

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

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

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

func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	logging.LogTrace(cb.logger, "ApplyZid", "entries", len(myZettel))
	for zid, gen := range myZettel {
		if !constraint(zid) {
			continue
		}
		if genMeta := gen.meta; genMeta != nil {
			if genMeta(zid) != nil {
				handle(zid)
			}
		}
	}
	return nil
}

func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	logging.LogTrace(cb.logger, "ApplyMeta", "entries", len(myZettel))
	for zid, gen := range myZettel {
		if !constraint(zid) {
			continue
		}
		if genMeta := gen.meta; genMeta != nil {
			if m := genMeta(zid); m != nil {
				updateMeta(m)
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164

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

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

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







|






|







143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164

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

func (cb *compBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
	st.Zettel = len(myZettel)
	logging.LogTrace(cb.logger, "ReadStats", "zettel", st.Zettel)
}

func getTitledMeta(zid id.Zid, title string) *meta.Meta {
	m := meta.New(zid)
	m.Set(meta.KeyTitle, meta.Value(title))
	return m
}
Changes to internal/box/compbox/log.go.
16
17
18
19
20
21
22

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


48
49
50
51
import (
	"bytes"
	"context"

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

)

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

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


		buf.WriteByte('\n')
	}
	return buf.Bytes()
}







>




















|




>
>




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

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

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

func genLogC(context.Context, *compBox) []byte {
	const tsFormat = "2006-01-02 15:04:05.999999"
	entries := kernel.Main.RetrieveLogEntries()
	var buf bytes.Buffer
	for _, entry := range entries {
		ts := entry.TS.Format(tsFormat)
		buf.WriteString(ts)
		for j := len(ts); j < len(tsFormat); j++ {
			buf.WriteByte('0')
		}
		buf.WriteByte(' ')
		buf.WriteString(logging.LevelStringPad(entry.Level))
		buf.WriteByte(' ')
		buf.WriteString(entry.Prefix)
		buf.WriteByte(' ')
		buf.WriteString(entry.Message)
		buf.WriteByte(' ')
		buf.WriteString(entry.Details)
		buf.WriteByte('\n')
	}
	return buf.Bytes()
}
Changes to internal/box/compbox/sx.go.
26
27
28
29
30
31
32



33




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

func genSxC(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Name|=Value>\n")



	fmt.Fprintf(&buf, "|Symbols|%d\n", sx.MakeSymbol("NIL").Factory().Size())




	return buf.Bytes()
}







>
>
>
|
>
>
>
>


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func genSxM(zid id.Zid) *meta.Meta {
	return getTitledMeta(zid, "Zettelstore Sx Engine")
}

func genSxC(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Name|=Value>\n")
	numSymbols := 0
	for pkg := range sx.AllPackages() {
		if size := pkg.Size(); size > 0 {
			fmt.Fprintf(&buf, "|Symbols in package %q|%d\n", pkg.Name(), size)
			numSymbols += size
		}
	}
	fmt.Fprintf(&buf, "|All symbols|%d\n", numSymbols)
	return buf.Bytes()
}
Changes to internal/box/constbox/base.sxn.
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
  (meta (@ (charset "utf-8")))
  (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta (@ (name "generator") (content "Zettelstore")))
  (meta (@ (name "format-detection") (content "telephone=no")))
  ,@META-HEADER
  (link (@ (rel "stylesheet") (href ,css-base-url)))
  (link (@ (rel "stylesheet") (href ,css-user-url)))
  ,@(ROLE-DEFAULT-meta (current-binding))

  (title ,title))
(body
  (nav (@ (class "zs-menu"))
    (a (@ (href ,home-url)) "Home")
    ,@(if with-auth
      `((div (@ (class "zs-dropdown"))
        (button "User")
        (nav (@ (class "zs-dropdown-content"))
          ,@(if user-is-valid
            `((a (@ (href ,user-zettel-url)) ,user-ident)
              (a (@ (href ,logout-url)) "Logout"))
            `((a (@ (href ,login-url)) "Login"))
          )
      )))
    )
    (div (@ (class "zs-dropdown"))
      (button "Lists")
      (nav (@ (class "zs-dropdown-content"))
        ,@list-urls
        ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh")))
    ))
    ,@(if new-zettel-links
      `((div (@ (class "zs-dropdown"))
        (button "New")
        (nav (@ (class "zs-dropdown-content"))
          ,@(map wui-link new-zettel-links)
       )))







|
>



















|







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
  (meta (@ (charset "utf-8")))
  (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta (@ (name "generator") (content "Zettelstore")))
  (meta (@ (name "format-detection") (content "telephone=no")))
  ,@META-HEADER
  (link (@ (rel "stylesheet") (href ,css-base-url)))
  (link (@ (rel "stylesheet") (href ,css-user-url)))
  ,@(ROLE-DEFAULT-meta (current-frame))
  ,@(let* ((frame (current-frame))(rem (resolve-symbol 'ROLE-EXTRA-meta frame))) (if (defined? rem) (rem frame)))
  (title ,title))
(body
  (nav (@ (class "zs-menu"))
    (a (@ (href ,home-url)) "Home")
    ,@(if with-auth
      `((div (@ (class "zs-dropdown"))
        (button "User")
        (nav (@ (class "zs-dropdown-content"))
          ,@(if user-is-valid
            `((a (@ (href ,user-zettel-url)) ,user-ident)
              (a (@ (href ,logout-url)) "Logout"))
            `((a (@ (href ,login-url)) "Login"))
          )
      )))
    )
    (div (@ (class "zs-dropdown"))
      (button "Lists")
      (nav (@ (class "zs-dropdown-content"))
        ,@list-urls
        ,@(if (symbol-bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh")))
    ))
    ,@(if new-zettel-links
      `((div (@ (class "zs-dropdown"))
        (button "New")
        (nav (@ (class "zs-dropdown-content"))
          ,@(map wui-link new-zettel-links)
       )))
Changes to internal/box/constbox/constbox.go.
13
14
15
16
17
18
19

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

// Package constbox puts zettel inside the executable.
package constbox

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

	"net/url"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

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

type constHeader map[string]string

type constZettel struct {
	header  constHeader
	content zettel.Content
}

type constBox struct {
	log      *logger.Logger
	number   int
	zettel   map[id.Zid]constZettel
	enricher box.Enricher
}

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

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

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

func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid")
	for zid := range cb.zettel {
		if constraint(zid) {
			handle(zid)
		}
	}
	return nil
}

func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyMeta")
	for zid, zettel := range cb.zettel {
		if constraint(zid) {
			m := meta.NewWithData(zid, zettel.header)
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

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

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

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

var constZettelMap = map[id.Zid]constZettel{
	id.ZidConfiguration: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Runtime Configuration",
			meta.KeyRole:       meta.ValueRoleConfiguration,







>








|









|
<















|









|



|









|









|


















|






|







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

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

// Package constbox puts zettel inside the executable.
package constbox

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

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

func init() {
	manager.Register(
		" const",
		func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &constBox{
				logger:   kernel.Main.GetLogger(kernel.BoxService).With("box", "const", "boxnum", cdata.Number),

				number:   cdata.Number,
				zettel:   constZettelMap,
				enricher: cdata.Enricher,
			}, nil
		})
}

type constHeader map[string]string

type constZettel struct {
	header  constHeader
	content zettel.Content
}

type constBox struct {
	logger   *slog.Logger
	number   int
	zettel   map[id.Zid]constZettel
	enricher box.Enricher
}

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

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

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

func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	logging.LogTrace(cb.logger, "ApplyZid", "entries", len(cb.zettel))
	for zid := range cb.zettel {
		if constraint(zid) {
			handle(zid)
		}
	}
	return nil
}

func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	logging.LogTrace(cb.logger, "ApplyMeta", "entries", len(cb.zettel))
	for zid, zettel := range cb.zettel {
		if constraint(zid) {
			m := meta.NewWithData(zid, zettel.header)
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

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

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

func (cb *constBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
	st.Zettel = len(cb.zettel)
	logging.LogTrace(cb.logger, "ReadStats", "zettel", st.Zettel)
}

var constZettelMap = map[id.Zid]constZettel{
	id.ZidConfiguration: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Runtime Configuration",
			meta.KeyRole:       meta.ValueRoleConfiguration,
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
			meta.KeyTitle:      "Zettelstore Dependencies",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeyCreated:    "20210504135842",
			meta.KeyModified:   "20250312154700",
		},
		zettel.NewContent(contentDependencies)},
	id.ZidBaseTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155100",
			meta.KeyModified:   "20241227212000",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.ZidLoginTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZidZettelTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155300",
			meta.KeyModified:   "20250214153100",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentZettelSxn)},
	id.ZidInfoTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20241127170500",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.ZidFormTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},
	id.ZidDeleteTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20241127170530",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ZidListTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230704122100",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ZidErrorTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Error HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,







|








|



















|









|









|









|









|







153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
			meta.KeyTitle:      "Zettelstore Dependencies",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxZmk,
			meta.KeyLang:       meta.ValueLangEN,
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeyCreated:    "20210504135842",
			meta.KeyModified:   "20250623150400",
		},
		zettel.NewContent(contentDependencies)},
	id.ZidBaseTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155100",
			meta.KeyModified:   "20250623131400",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.ZidLoginTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240219145200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZidZettelTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155300",
			meta.KeyModified:   "20250623131300",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentZettelSxn)},
	id.ZidInfoTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20250623131300",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.ZidFormTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20250612180300",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},
	id.ZidDeleteTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20250612180200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ZidListTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230704122100",
			meta.KeyModified:   "20250612180200",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ZidErrorTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Error HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
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
		zettel.NewContent(contentStartCodeSxn)},
	id.ZidSxnBase: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Sxn Base Code",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230619132800",
			meta.KeyModified:   "20241118173500",
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeyPrecursor:  id.ZidSxnPrelude.String(),
		},
		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",







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



|







253
254
255
256
257
258
259
260












261
262
263
264
265
266
267
268
269
270
271
		zettel.NewContent(contentStartCodeSxn)},
	id.ZidSxnBase: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Sxn Base Code",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230619132800",
			meta.KeyModified:   "20250623131700",












			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseCodeSxn)},
	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

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







<
<
<







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 base.css
var contentBaseCSS []byte

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

//go:embed menu_lists.zettel
Changes to internal/box/constbox/delete.sxn.
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  ,@(if incoming
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "If you delete this zettel, incoming references from the following zettel will become invalid.")
      (ul ,@(map wui-item-link incoming))
    ))
  )
  ,@(if (and (bound? 'useless) useless)
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.")
      (ul ,@(map wui-item useless))
    ))
  )
  ,(wui-meta-desc metapairs)







|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  ,@(if incoming
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "If you delete this zettel, incoming references from the following zettel will become invalid.")
      (ul ,@(map wui-item-link incoming))
    ))
  )
  ,@(if (and (symbol-bound? 'useless) useless)
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.")
      (ul ,@(map wui-item useless))
    ))
  )
  ,(wui-meta-desc metapairs)
Changes to internal/box/constbox/dependencies.zettel.
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146


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

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

; URL & Source Sx
: [[https://t73f.de/r/sx]]
; URL & Source SxWebs
: [[https://t73f.de/r/sxwebs]]
; URL & Source Webs
: [[https://t73f.de/r/webs]]
; URL & Source Zero
: [[https://t73f.de/r/zero]]
; URL & Source Zettelstore-Client
: [[https://t73f.de/r/zsc]]


; License:
: European Union Public License, version 1.2 (EUPL v1.2), or later.







|













>
>


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

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

; URL & Source Sx
: [[https://t73f.de/r/sx]]
; URL & Source SxWebs
: [[https://t73f.de/r/sxwebs]]
; URL & Source Webs
: [[https://t73f.de/r/webs]]
; URL & Source Zero
: [[https://t73f.de/r/zero]]
; URL & Source Zettelstore-Client
: [[https://t73f.de/r/zsc]]
; URL & Source Zsx
: [[https://t73f.de/r/zsx]]
; License:
: European Union Public License, version 1.2 (EUPL v1.2), or later.
Changes to internal/box/constbox/form.sxn.
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax")
              (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.")
              (placeholder "syntax..") (value ,meta-syntax) (dir "auto")
      ,@(if syntax-data '((list "zs-syntax-data")))
    ))
    ,@(wui-datalist "zs-syntax-data" syntax-data)
  )
  ,@(if (bound? 'content)
    `((div
      (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20")
                   (title "Zettel content, according to the given syntax")
                   (placeholder "Zettel content..") (dir "auto")) ,content)
    ))
  )







|







43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax")
              (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.")
              (placeholder "syntax..") (value ,meta-syntax) (dir "auto")
      ,@(if syntax-data '((list "zs-syntax-data")))
    ))
    ,@(wui-datalist "zs-syntax-data" syntax-data)
  )
  ,@(if (symbol-bound? 'content)
    `((div
      (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20")
                   (title "Zettel content, according to the given syntax")
                   (placeholder "Zettel content..") (dir "auto")) ,content)
    ))
  )
Changes to internal/box/constbox/info.sxn.
11
12
13
14
15
16
17
18
19
20
21


22
23
24
25
26
27
28
29
30
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      (@H " / ") (a (@ (href ,context-full-url)) "Full")
      ,@(if (bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      ,@(ROLE-DEFAULT-actions (current-binding))


      ,@(if (bound? 'reindex-url) `((@H " &#183; ") (a (@ (href ,reindex-url)) "Reindex")))
      ,@(if (bound? 'delete-url) `((@H " &#183; ") (a (@ (href ,delete-url)) "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
  (h2 "References")
  ,@(if local-links `((h3 "Local")    (ul ,@(map wui-local-link local-links)))) 
  ,@(if query-links `((h3 "Queries")  (ul ,@(map wui-item-link query-links))))







|
|
<
|
>
>
|
|







11
12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30
31
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      ,@(if (symbol-bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      (@H " &#183; ") (a (@ (href ,context-full-url)) "Full Context")

      ,@(ROLE-DEFAULT-actions (current-frame))
      ,@(let* ((frame (current-frame))(rea (resolve-symbol 'ROLE-EXTRA-actions frame))) (if (defined? rea) (rea frame)))
      ,@(if (symbol-bound? 'version-url) `((@H " &#183; ") (a (@ (href ,version-url)) "Version")))
      ,@(if (symbol-bound? 'reindex-url) `((@H " &#183; ") (a (@ (href ,reindex-url)) "Reindex")))
      ,@(if (symbol-bound? 'delete-url) `((@H " &#183; ") (a (@ (href ,delete-url)) "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
  (h2 "References")
  ,@(if local-links `((h3 "Local")    (ul ,@(map wui-local-link local-links)))) 
  ,@(if query-links `((h3 "Queries")  (ul ,@(map wui-item-link query-links))))
Changes to internal/box/constbox/listzettel.sxn.
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

`(article
  (header (h1 ,heading))
  (search (form (@ (action ,search-url))
    (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query)
              (title "Contains the search that leads to the list below. You're allowed to modify it")
              (placeholder "Search..") (value ,query-value) (dir "auto")))))
  ,@(if (bound? 'tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel))
    )
  ,@(if (bound? 'create-tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel))
    )
  ,@(if (bound? 'role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel))
    )
  ,@(if (bound? 'create-role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel))
    )
  ,@content
  ,@endnotes
  (form (@ (action ,(if (bound? 'create-url) create-url)))
      ,(if (bound? 'data-url)
          `(@L "Other encodings"
               ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ")
               (a (@ (href ,data-url)) "data")
               ", "
               (a (@ (href ,plain-url)) "plain")
           )
      )
      ,@(if (bound? 'create-url)
        `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value)))
          (input (@ (type "hidden") (name ,query-key-seed) (value ,seed)))
          (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel")))
        )
      )
  )
)







|


|


|


|




|
|







|







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

`(article
  (header (h1 ,heading))
  (search (form (@ (action ,search-url))
    (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query)
              (title "Contains the search that leads to the list below. You're allowed to modify it")
              (placeholder "Search..") (value ,query-value) (dir "auto")))))
  ,@(if (symbol-bound? 'tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel))
    )
  ,@(if (symbol-bound? 'create-tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel))
    )
  ,@(if (symbol-bound? 'role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel))
    )
  ,@(if (symbol-bound? 'create-role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel))
    )
  ,@content
  ,@endnotes
  (form (@ (action ,(if (symbol-bound? 'create-url) create-url)))
      ,(if (symbol-bound? 'data-url)
          `(@L "Other encodings"
               ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ")
               (a (@ (href ,data-url)) "data")
               ", "
               (a (@ (href ,plain-url)) "plain")
           )
      )
      ,@(if (symbol-bound? 'create-url)
        `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value)))
          (input (@ (type "hidden") (name ,query-key-seed) (value ,seed)))
          (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel")))
        )
      )
  )
)
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/wuicode.sxn.
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
;; identifier. It is used in the base template to update the metadata of the
;; HTML page to include some role specific CSS code.
;; Referenced in function "ROLE-DEFAULT-meta".
(defvar CSS-ROLE-map '())

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

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

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

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

;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role".
(defun ROLE-role-actions (binding)
    `(,@(ROLE-DEFAULT-actions binding)
      ,@(let ((title (binding-lookup 'title binding)))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel"))
             )
        )
    )
)

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







|
|












|
|

<
<
|

|





|
<
|








|
<
|










|
|

|




|



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


93
94
95
96
97
98
99
100
101

102
103
104
105
106
107
108
109
110
111

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
;; identifier. It is used in the base template to update the metadata of the
;; HTML page to include some role specific CSS code.
;; Referenced in function "ROLE-DEFAULT-meta".
(defvar CSS-ROLE-map '())

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

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

;; ROLE-DEFAULT-actions returns the default text for actions.
(defun ROLE-DEFAULT-actions (frame)
    `(,@(let ((copy-url (resolve-symbol 'copy-url frame)))
             (if (defined? copy-url) `((@H " &#183; ") (a (@ (href ,copy-url)) "Copy"))))


      ,@(let ((sequel-url (resolve-symbol 'sequel-url frame)))
             (if (defined? sequel-url) `((@H " &#183; ") (a (@ (href ,sequel-url)) "Sequel"))))
      ,@(let ((folge-url (resolve-symbol 'folge-url frame)))
             (if (defined? folge-url) `((@H " &#183; ") (a (@ (href ,folge-url)) "Folge"))))
    )
)

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

    `(,@(let ((title (resolve-symbol 'title frame)))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel"))
             )
        )
    )
)

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

    `(,@(let ((title (resolve-symbol 'title frame)))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel"))
             )
        )
    )
)

;; ROLE-DEFAULT-heading returns the default text for headings, below the
;; references of a zettel. In most cases it should be called from an
;; overwriting function.
(defun ROLE-DEFAULT-heading (frame)
    `(,@(let ((meta-url (resolve-symbol 'meta-url frame)))
           (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url))))
      ,@(let ((urls (resolve-symbol 'urls frame)))
           (if (defined? urls)
               (map (lambda (u) `(@L (br) ,(car u) ": " ,(url-to-html (cdr u)))) urls)
           )
        )
      ,@(let ((meta-author (resolve-symbol 'meta-author frame)))
           (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author)))
    )
)
Changes to internal/box/constbox/zettel.sxn.
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

26

27
28
29
30
31

32
33
34
35
36
37
38
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header
    (h1 ,heading)
    (div (@ (class "zs-meta"))
      ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " &#183; ")))
      ,zid (@H " &#183; ")
      (a (@ (href ,info-url)) "Info") (@H " &#183; ")
      "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role)))
          ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))

      ,@(ROLE-DEFAULT-actions (current-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))

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







|


|
|



>
|
>




|
>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header
    (h1 ,heading)
    (div (@ (class "zs-meta"))
      ,@(if (symbol-bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " &#183; ")))
      ,zid (@H " &#183; ")
      (a (@ (href ,info-url)) "Info") (@H " &#183; ")
      "(" ,@(if (symbol-bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role)))
          ,@(if (and (symbol-bound? 'folge-role-url) (symbol-bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      ,@(ROLE-DEFAULT-actions (current-frame))
      ,@(let* ((frame (current-frame))(rea (resolve-symbol 'ROLE-EXTRA-actions frame))) (if (defined? rea) (rea frame)))
      ,@(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-frame))
      ,@(let* ((frame (current-frame))(reh (resolve-symbol 'ROLE-EXTRA-heading frame))) (if (defined? reh) (reh frame)))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links sequel-links back-links successor-links subordinate-links)
    `((nav
      ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
Changes to internal/box/dirbox/dirbox.go.
13
14
15
16
17
18
19

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

// Package dirbox provides a directory-based zettel box.
package dirbox

import (
	"context"
	"errors"

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

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/box/notify"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		var log *logger.Logger
		if krnl := kernel.Main; krnl != nil {
			log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child()
		}
		path := getDirPath(u)
		if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
			return nil, err
		}
		dp := dirBox{
			log:        log,
			number:     cdata.Number,
			location:   u.String(),
			readonly:   box.GetQueryBool(u, "readonly"),
			cdata:      *cdata,
			dir:        path,
			notifySpec: getDirSrvInfo(log, u.Query().Get("type")),
			fSrvs:      makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))),
		}
		return &dp, nil
	})
}

func makePrime(n uint32) uint32 {







>












|






|

|






|





|







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

// Package dirbox provides a directory-based zettel box.
package dirbox

import (
	"context"
	"errors"
	"log/slog"
	"net/url"
	"os"
	"path/filepath"
	"sync"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/box/notify"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		var logger *slog.Logger
		if krnl := kernel.Main; krnl != nil {
			logger = krnl.GetLogger(kernel.BoxService).With("box", "dir", "boxnum", cdata.Number)
		}
		path := getDirPath(u)
		if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
			return nil, err
		}
		dp := dirBox{
			logger:     logger,
			number:     cdata.Number,
			location:   u.String(),
			readonly:   box.GetQueryBool(u, "readonly"),
			cdata:      *cdata,
			dir:        path,
			notifySpec: getDirSrvInfo(logger, u.Query().Get("type")),
			fSrvs:      makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))),
		}
		return &dp, nil
	})
}

func makePrime(n uint32) uint32 {
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
const (
	_ notifyTypeSpec = iota
	dirNotifyAny
	dirNotifySimple
	dirNotifyFS
)

func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec {
	for range 2 {
		switch notifyType {
		case kernel.BoxDirTypeNotify:
			return dirNotifyFS
		case kernel.BoxDirTypeSimple:
			return dirNotifySimple
		default:
			notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string)
		}
	}
	log.Error().Str("notifyType", notifyType).Msg("Unable to set notify type, using a default")
	return dirNotifySimple
}

func getDirPath(u *url.URL) string {
	if u.Opaque != "" {
		return filepath.Clean(u.Opaque)
	}
	return filepath.Clean(u.Path)
}

// dirBox uses a directory to store zettel as files.
type dirBox struct {
	log        *logger.Logger
	number     int
	location   string
	readonly   bool
	cdata      manager.ConnectData
	dir        string
	notifySpec notifyTypeSpec
	dirSrv     *notify.DirService







|










|












|







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
const (
	_ notifyTypeSpec = iota
	dirNotifyAny
	dirNotifySimple
	dirNotifyFS
)

func getDirSrvInfo(logger *slog.Logger, notifyType string) notifyTypeSpec {
	for range 2 {
		switch notifyType {
		case kernel.BoxDirTypeNotify:
			return dirNotifyFS
		case kernel.BoxDirTypeSimple:
			return dirNotifySimple
		default:
			notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string)
		}
	}
	logger.Error("Unable to set notify type, using a default", "notifyType", notifyType)
	return dirNotifySimple
}

func getDirPath(u *url.URL) string {
	if u.Opaque != "" {
		return filepath.Clean(u.Opaque)
	}
	return filepath.Clean(u.Path)
}

// dirBox uses a directory to store zettel as files.
type dirBox struct {
	logger     *slog.Logger
	number     int
	location   string
	readonly   bool
	cdata      manager.ConnectData
	dir        string
	notifySpec notifyTypeSpec
	dirSrv     *notify.DirService
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216

func (dp *dirBox) Start(context.Context) error {
	dp.mxCmds.Lock()
	defer dp.mxCmds.Unlock()
	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
	for i := range dp.fSrvs {
		cc := make(chan fileCmd)
		go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc)
		dp.fCmds = append(dp.fCmds, cc)
	}

	var notifier notify.Notifier
	var err error
	switch dp.notifySpec {
	case dirNotifySimple:
		notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir)
	default:
		notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir)
	}
	if err != nil {
		dp.log.Error().Err(err).Msg("Unable to create directory supervisor")
		dp.stopFileServices()
		return err
	}
	dp.dirSrv = notify.NewDirService(
		dp,
		dp.log.Clone().Str("sub", "dirsrv").Child(),
		notifier,
		dp.cdata.Notify,
	)
	dp.dirSrv.Start()
	return nil
}

func (dp *dirBox) Refresh(_ context.Context) {
	dp.dirSrv.Refresh()
	dp.log.Trace().Msg("Refresh")
}

func (dp *dirBox) Stop(_ context.Context) {
	dirSrv := dp.dirSrv
	dp.dirSrv = nil
	if dirSrv != nil {
		dirSrv.Stop()
	}
	dp.stopFileServices()
}

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

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

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







|







|

|


|





|









|



















|







154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217

func (dp *dirBox) Start(context.Context) error {
	dp.mxCmds.Lock()
	defer dp.mxCmds.Unlock()
	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
	for i := range dp.fSrvs {
		cc := make(chan fileCmd)
		go fileService(i, dp.logger.With("sub", "file", "fn", i), dp.dir, cc)
		dp.fCmds = append(dp.fCmds, cc)
	}

	var notifier notify.Notifier
	var err error
	switch dp.notifySpec {
	case dirNotifySimple:
		notifier, err = notify.NewSimpleDirNotifier(dp.logger.With("notify", "simple"), dp.dir)
	default:
		notifier, err = notify.NewFSDirNotifier(dp.logger.With("notify", "fs"), dp.dir)
	}
	if err != nil {
		dp.logger.Error("Unable to create directory supervisor", "err", err)
		dp.stopFileServices()
		return err
	}
	dp.dirSrv = notify.NewDirService(
		dp,
		dp.logger.With("sub", "dirsrv"),
		notifier,
		dp.cdata.Notify,
	)
	dp.dirSrv.Start()
	return nil
}

func (dp *dirBox) Refresh(_ context.Context) {
	dp.dirSrv.Refresh()
	logging.LogTrace(dp.logger, "Refresh")
}

func (dp *dirBox) Stop(_ context.Context) {
	dirSrv := dp.dirSrv
	dp.dirSrv = nil
	if dirSrv != nil {
		dirSrv.Stop()
	}
	dp.stopFileServices()
}

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

func (dp *dirBox) notifyChanged(zid id.Zid, reason box.UpdateReason) {
	if notify := dp.cdata.Notify; notify != nil {
		logging.LogTrace(dp.logger, "notifyChanged", "zid", zid, "reason", reason)
		notify(dp, zid, reason)
	}
}

func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd {
	// Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
	sum := 2166136261 ^ uint32(zid)
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
	dp.updateEntryFromMetaContent(&entry, meta, zettel.Content)

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

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

func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool {
	return dp.dirSrv.GetDirEntry(zid).IsValid()
}

func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	entries := dp.dirSrv.GetDirEntries(constraint)
	dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid")
	for _, entry := range entries {
		handle(entry.Zid)
	}
	return nil
}

func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	entries := dp.dirSrv.GetDirEntries(constraint)
	dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta")

	// The following loop could be parallelized if needed for performance.
	for _, entry := range entries {
		m, err := dp.srvGetMeta(ctx, entry, entry.Zid)
		if err != nil {
			dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta")
			return err
		}
		dp.cdata.Enricher.Enrich(ctx, m, dp.number)
		handle(m)
	}
	return nil
}







|













|









|








|





|







243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
	dp.updateEntryFromMetaContent(&entry, meta, zettel.Content)

	err = dp.srvSetZettel(ctx, &entry, zettel)
	if err == nil {
		err = dp.dirSrv.UpdateDirEntry(&entry)
	}
	dp.notifyChanged(meta.Zid, box.OnZettel)
	logging.LogTrace(dp.logger, "CreateZettel", logging.Err(err), "zid", meta.Zid)
	return meta.Zid, err
}

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

func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool {
	return dp.dirSrv.GetDirEntry(zid).IsValid()
}

func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	entries := dp.dirSrv.GetDirEntries(constraint)
	logging.LogTrace(dp.logger, "ApplyZid", "entries", len(entries))
	for _, entry := range entries {
		handle(entry.Zid)
	}
	return nil
}

func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	entries := dp.dirSrv.GetDirEntries(constraint)
	logging.LogTrace(dp.logger, "ApplyMeta", "entries", len(entries))

	// The following loop could be parallelized if needed for performance.
	for _, entry := range entries {
		m, err := dp.srvGetMeta(ctx, entry, entry.Zid)
		if err != nil {
			logging.LogTrace(dp.logger, "ApplyMeta/getMeta", "err", err)
			return err
		}
		dp.cdata.Enricher.Enrich(ctx, m, dp.number)
		handle(m)
	}
	return nil
}
310
311
312
313
314
315
316
317



318
319
320
321
322
323
324
325
326
327
328
329
	}
	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)
	if err == nil {
		dp.notifyChanged(zid, box.OnZettel)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel")
	return err
}

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








|
>
>
>
|



|







311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
	}
	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)
	err := dp.dirSrv.UpdateDirEntry(entry)
	if err != nil {
		return err
	}
	err = dp.srvSetZettel(ctx, entry, zettel)
	if err == nil {
		dp.notifyChanged(zid, box.OnZettel)
	}
	logging.LogTrace(dp.logger, "UpdateZettel", "zid", zid, logging.Err(err))
	return err
}

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

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

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







|






|

352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {
		dp.notifyChanged(zid, box.OnDelete)
	}
	logging.LogTrace(dp.logger, "DeleteZettel", "zid", zid, logging.Err(err))
	return err
}

func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = dp.readonly
	st.Zettel = dp.dirSrv.NumDirEntries()
	logging.LogTrace(dp.logger, "ReadStats", "zettel", st.Zettel)
}
Changes to internal/box/dirbox/service.go.
13
14
15
16
17
18
19

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

package dirbox

import (
	"context"
	"fmt"
	"io"

	"os"
	"path/filepath"
	"time"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/box/filebox"
	"zettelstore.de/z/internal/box/notify"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/zettel"
)

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

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

type fileCmd interface {
	run(string)
}

const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing.







>











<



|




|



|



|







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

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

package dirbox

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"time"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/box/filebox"
	"zettelstore.de/z/internal/box/notify"
	"zettelstore.de/z/internal/kernel"

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

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

	logger.Debug("File service started", "i", i, "dirpath", dirPath)
	for cmd := range cmds {
		cmd.run(dirPath)
	}
	logger.Debug("File service stopped", "i", i, "dirpath", dirPath)
}

type fileCmd interface {
	run(string)
}

const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing.
Changes to internal/box/filebox/filebox.go.
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		path := getFilepathFromURL(u)
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".zip" {
			return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String())
		}
		return &zipBox{
			log: kernel.Main.GetLogger(kernel.BoxService).Clone().
				Str("box", "zip").Int("boxnum", int64(cdata.Number)).Child(),
			number:   cdata.Number,
			name:     path,
			enricher: cdata.Enricher,
			notify:   cdata.Notify,
		}, nil
	})
}







|
<







32
33
34
35
36
37
38
39

40
41
42
43
44
45
46
	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		path := getFilepathFromURL(u)
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".zip" {
			return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String())
		}
		return &zipBox{
			logger:   kernel.Main.GetLogger(kernel.BoxService).With("box", "zip", "boxnum", cdata.Number),

			number:   cdata.Number,
			name:     path,
			enricher: cdata.Enricher,
			notify:   cdata.Notify,
		}, nil
	})
}
Changes to internal/box/filebox/zipbox.go.
14
15
16
17
18
19
20

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

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

	"strings"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/notify"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

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








>








|





|







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

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

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/notify"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

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

66
67
68
69
70
71
72
73


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
}

func (zb *zipBox) Start(context.Context) error {
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return err
	}
	reader.Close()


	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) {
	zb.dirSrv.Refresh()
	zb.log.Trace().Msg("Refresh")
}

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

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

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

	contentName := entry.ContentName
	if metaName := entry.MetaName; metaName == "" {







|
>
>
|
|






|
















|







67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
}

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

func (zb *zipBox) Refresh(_ context.Context) {
	zb.dirSrv.Refresh()
	logging.LogTrace(zb.logger, "Refresh")
}

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

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

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

	contentName := entry.ContentName
	if metaName := entry.MetaName; metaName == "" {
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
			if err != nil {
				return zettel.Zettel{}, err
			}
		}
	}

	CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles)
	zb.log.Trace().Zid(zid).Msg("GetZettel")
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil
}

func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool {
	return zb.dirSrv.GetDirEntry(zid).IsValid()
}

func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	entries := zb.dirSrv.GetDirEntries(constraint)
	zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid")
	for _, entry := range entries {
		handle(entry.Zid)
	}
	return nil
}

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
}

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

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

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

func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) {
	var inMeta bool
	if metaName := entry.MetaName; metaName == "" {
		contentName := entry.ContentName
		contentExt := entry.ContentExt







|









|











<

|











|










|






|







133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
			if err != nil {
				return zettel.Zettel{}, err
			}
		}
	}

	CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles)
	logging.LogTrace(zb.logger, "GetZettel", "zid", zid)
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil
}

func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool {
	return zb.dirSrv.GetDirEntry(zid).IsValid()
}

func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	entries := zb.dirSrv.GetDirEntries(constraint)
	logging.LogTrace(zb.logger, "ApplyZid", "entries", len(entries))
	for _, entry := range entries {
		handle(entry.Zid)
	}
	return nil
}

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
	}

	entries := zb.dirSrv.GetDirEntries(constraint)
	logging.LogTrace(zb.logger, "ApplyMeta", "entries", len(entries))
	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 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)
	if !entry.IsValid() {
		err = box.ErrZettelNotFound{Zid: zid}
	}
	logging.LogTrace(zb.logger, "DeleteZettel", "err", err)
	return err
}

func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
	st.Zettel = zb.dirSrv.NumDirEntries()
	logging.LogTrace(zb.logger, "ReadStats", "zettel", st.Zettel)
}

func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) {
	var inMeta bool
	if metaName := entry.MetaName; metaName == "" {
		contentName := entry.ContentName
		contentExt := entry.ContentExt
222
223
224
225
226
227
228

229



230
231
}

func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) {
	f, err := reader.Open(name)
	if err != nil {
		return nil, err
	}

	defer f.Close()



	return io.ReadAll(f)
}







>
|
>
>
>
|

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

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)
	err2 := f.Close()
	if err == nil {
		err = err2
	}
	return data, err
}
Changes to internal/box/manager/box.go.
19
20
21
22
23
24
25

26
27
28
29
30
31
32
	"strings"

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

	"zettelstore.de/z/internal/box"

	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

// Conatains all box.Box related functions

// Location returns some information where the box is located.







>







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

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

// Conatains all box.Box related functions

// Location returns some information where the box is located.
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
		return box.CanCreateZettel(ctx)
	}
	return false
}

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

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel")
	if err := mgr.checkContinue(ctx); err != nil {
		return zettel.Zettel{}, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.getZettel(ctx, zid)
}







|


















|







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
		return box.CanCreateZettel(ctx)
	}
	return false
}

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

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	mgr.mgrLogger.Debug("GetZettel", "zid", zid)
	if err := mgr.checkContinue(ctx); err != nil {
		return zettel.Zettel{}, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.getZettel(ctx, zid)
}
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
		}
	}
	return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
}

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

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







|

















|







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
		}
	}
	return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
}

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

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (*idset.Set, error) {
	mgr.mgrLogger.Debug("FetchZids")
	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.fetchZids(ctx)
}
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
			return nil, err
		}
	}
	return result, nil
}

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

// GetMeta returns just the metadata of the zettel with the given identifier.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta")
	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}

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

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

	compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq)
	if result := compSearch.Result(); result != nil {
		mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta")
		return result, nil
	}
	selected := map[id.Zid]*meta.Meta{}
	for _, term := range compSearch.Terms {
		rejected := idset.New()
		handleMeta := func(m *meta.Meta) {
			zid := m.Zid
			if rejected.Contains(zid) {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected")
				return
			}
			if _, ok := selected[zid]; ok {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected")
				return
			}
			if compSearch.PreMatch(m) && term.Match(m) {
				selected[zid] = m
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match")
			} else {
				rejected.Add(zid)
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject")
			}
		}
		for _, p := range mgr.boxes {
			if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil {
				return nil, err2
			}
		}
	}
	result := make([]*meta.Meta, 0, len(selected))
	for _, m := range selected {
		result = append(result, m)
	}
	result = compSearch.AfterSearch(result)
	mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta")
	return result, nil
}

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

}

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







|















|
















<
|
<








|








|



|




|


|













|



















|







142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181

182

183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
			return nil, err
		}
	}
	return result, nil
}

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

// GetMeta returns just the metadata of the zettel with the given identifier.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrLogger.Debug("GetMeta", "zid", zid)
	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}

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

// SelectMeta returns all zettel meta data that match the selection
// criteria. The result is ordered by descending zettel id.
func (mgr *Manager) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) {

	mgr.mgrLogger.Debug("SelectMeta", "query", q)

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

	compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq)
	if result := compSearch.Result(); result != nil {
		logging.LogTrace(mgr.mgrLogger, "found without ApplyMeta", "count", len(result))
		return result, nil
	}
	selected := map[id.Zid]*meta.Meta{}
	for _, term := range compSearch.Terms {
		rejected := idset.New()
		handleMeta := func(m *meta.Meta) {
			zid := m.Zid
			if rejected.Contains(zid) {
				logging.LogTrace(mgr.mgrLogger, "SelectMeta/alreadyRejected", "zid", zid)
				return
			}
			if _, ok := selected[zid]; ok {
				logging.LogTrace(mgr.mgrLogger, "SelectMeta/alreadySelected", "zid", zid)
				return
			}
			if compSearch.PreMatch(m) && term.Match(m) {
				selected[zid] = m
				logging.LogTrace(mgr.mgrLogger, "SelectMeta/match", "zid", zid)
			} else {
				rejected.Add(zid)
				logging.LogTrace(mgr.mgrLogger, "SelectMeta/reject", "zid", zid)
			}
		}
		for _, p := range mgr.boxes {
			if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil {
				return nil, err2
			}
		}
	}
	result := make([]*meta.Meta, 0, len(selected))
	for _, m := range selected {
		result = append(result, m)
	}
	result = compSearch.AfterSearch(result)
	logging.LogTrace(mgr.mgrLogger, "found with ApplyMeta", "count", len(result))
	return result, nil
}

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

}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	mgr.mgrLogger.Debug("UpdateZettel", "zid", zettel.Meta.Zid)
	if err := mgr.checkContinue(ctx); err != nil {
		return err
	}
	return mgr.updateZettel(ctx, zettel)
}
func (mgr *Manager) updateZettel(ctx context.Context, zettel zettel.Zettel) error {
	if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox {
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
		}
	}
	return false
}

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







|







274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
		}
	}
	return false
}

// DeleteZettel removes the zettel from the box.
func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mgr.mgrLogger.Debug("DeleteZettel", "zid", zid)
	if err := mgr.checkContinue(ctx); err != nil {
		return err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zid)
Changes to internal/box/manager/indexer.go.
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

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

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager/store"
	"zettelstore.de/z/internal/kernel"

	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/zettel"
	"zettelstore.de/z/strfun"
)

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

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

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

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

// idxIndexer runs in the background and updates the index data structures.
// This is the main service of the idxIndexer.
func (mgr *Manager) idxIndexer() {
	// Something may panic. Ensure a running indexer.







<










>









|
|
<
<







|
|
<
<







|
|
<
<







|
|
<
<







11
12
13
14
15
16
17

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


40
41
42
43
44
45
46
47
48


49
50
51
52
53
54
55
56
57


58
59
60
61
62
63
64
65
66


67
68
69
70
71
72
73
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

import (
	"context"

	"net/url"
	"time"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager/store"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/zettel"
	"zettelstore.de/z/strfun"
)

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchEqual(word string) *idset.Set {
	found := mgr.idxStore.SearchEqual(word)
	mgr.idxLogger.Debug("SearchEqual", "word", word, "found", found.Length())
	logging.LogTrace(mgr.idxLogger, "IDs", "ids", found)


	return found
}

// SearchPrefix returns all zettel that have a word with the given prefix.
// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchPrefix(prefix string) *idset.Set {
	found := mgr.idxStore.SearchPrefix(prefix)
	mgr.idxLogger.Debug("SearchPrefix", "prefix", prefix, "found", found.Length())
	logging.LogTrace(mgr.idxLogger, "IDs", "ids", found)


	return found
}

// SearchSuffix returns all zettel that have a word with the given suffix.
// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchSuffix(suffix string) *idset.Set {
	found := mgr.idxStore.SearchSuffix(suffix)
	mgr.idxLogger.Debug("SearchSuffix", "suffix", suffix, "found", found.Length())
	logging.LogTrace(mgr.idxLogger, "IDs", "ids", found)


	return found
}

// SearchContains returns all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchContains(s string) *idset.Set {
	found := mgr.idxStore.SearchContains(s)
	mgr.idxLogger.Debug("SearchContains", "s", s, "found", found.Length())
	logging.LogTrace(mgr.idxLogger, "IDs", "ids", found)


	return found
}

// idxIndexer runs in the background and updates the index data structures.
// This is the main service of the idxIndexer.
func (mgr *Manager) idxIndexer() {
	// Something may panic. Ensure a running indexer.
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
func (mgr *Manager) idxWorkService(ctx context.Context) {
	var start time.Time
	for {
		switch action, zid, lastReload := mgr.idxAr.Dequeue(); action {
		case arNothing:
			return
		case arReload:
			mgr.idxLog.Debug().Msg("reload")
			zids, err := mgr.FetchZids(ctx)
			if err == nil {
				start = time.Now()
				mgr.idxAr.Reload(zids)
				mgr.idxMx.Lock()
				mgr.idxLastReload = time.Now().Local()
				mgr.idxSinceReload = 0
				mgr.idxMx.Unlock()
			}
		case arZettel:
			mgr.idxLog.Debug().Zid(zid).Msg("zettel")
			zettel, err := mgr.GetZettel(ctx, zid)
			if err != nil {
				// Zettel was deleted or is not accessible b/c of other reasons
				mgr.idxLog.Trace().Zid(zid).Msg("delete")
				mgr.idxDeleteZettel(ctx, zid)
				continue
			}
			mgr.idxLog.Trace().Zid(zid).Msg("update")
			mgr.idxUpdateZettel(ctx, zettel)
			mgr.idxMx.Lock()
			if lastReload {
				mgr.idxDurReload = time.Since(start)
			}
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()







|










|



|



|







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
func (mgr *Manager) idxWorkService(ctx context.Context) {
	var start time.Time
	for {
		switch action, zid, lastReload := mgr.idxAr.Dequeue(); action {
		case arNothing:
			return
		case arReload:
			mgr.idxLogger.Debug("reload")
			zids, err := mgr.FetchZids(ctx)
			if err == nil {
				start = time.Now()
				mgr.idxAr.Reload(zids)
				mgr.idxMx.Lock()
				mgr.idxLastReload = time.Now().Local()
				mgr.idxSinceReload = 0
				mgr.idxMx.Unlock()
			}
		case arZettel:
			mgr.idxLogger.Debug("zettel", "zid", zid)
			zettel, err := mgr.GetZettel(ctx, zid)
			if err != nil {
				// Zettel was deleted or is not accessible b/c of other reasons
				logging.LogTrace(mgr.idxLogger, "delete", "zid", zid)
				mgr.idxDeleteZettel(ctx, zid)
				continue
			}
			logging.LogTrace(mgr.idxLogger, "update", "zid", zid)
			mgr.idxUpdateZettel(ctx, zettel)
			mgr.idxMx.Lock()
			if lastReload {
				mgr.idxDurReload = time.Since(start)
			}
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()
Changes to internal/box/manager/manager.go.
13
14
15
16
17
18
19

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

// Package manager coordinates the various boxes and indexes of a Zettelstore.
package manager

import (
	"context"
	"io"

	"net/url"
	"sync"
	"time"

	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager/mapstore"
	"zettelstore.de/z/internal/box/manager/store"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
)

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







>














|







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

// Package manager coordinates the various boxes and indexes of a Zettelstore.
package manager

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

	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager/mapstore"
	"zettelstore.de/z/internal/box/manager/store"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
)

// ConnectData contains all administration related values.
type ConnectData struct {
	Number   int // number of the box, starting with 1.
	Config   config.Config
	Enricher box.Enricher
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
		panic(scheme)
	}
	registry[scheme] = create
}

// Manager is a coordinating box.
type Manager struct {
	mgrLog       *logger.Logger
	stateMx      sync.RWMutex
	state        box.StartState
	mgrMx        sync.RWMutex
	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys *set.Set[string] // Set of property key names

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

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







|












|
|
|
|







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
		panic(scheme)
	}
	registry[scheme] = create
}

// Manager is a coordinating box.
type Manager struct {
	mgrLogger    *slog.Logger
	stateMx      sync.RWMutex
	state        box.StartState
	mgrMx        sync.RWMutex
	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys *set.Set[string] // Set of property key names

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

	// Indexer stats data
	idxMx          sync.RWMutex
	idxLastReload  time.Time
	idxDurReload   time.Duration
	idxSinceReload uint64
}
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
	descrs := meta.GetSortedKeyDescriptions()
	propertyKeys := set.New[string]()
	for _, kd := range descrs {
		if kd.IsProperty() {
			propertyKeys.Add(kd.Name)
		}
	}
	boxLog := kernel.Main.GetLogger(kernel.BoxService)
	mgr := &Manager{
		mgrLog:       boxLog.Clone().Str("box", "manager").Child(),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

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

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







|

|




|
|
|
|







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
	descrs := meta.GetSortedKeyDescriptions()
	propertyKeys := set.New[string]()
	for _, kd := range descrs {
		if kd.IsProperty() {
			propertyKeys.Add(kd.Name)
		}
	}
	boxLogger := kernel.Main.GetLogger(kernel.BoxService)
	mgr := &Manager{
		mgrLogger:    boxLogger.With("box", "manager"),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

		idxLogger: boxLogger.With("box", "index"),
		idxStore:  createIdxStore(rtConfig),
		idxAr:     newAnteroomQueue(1000),
		idxReady:  make(chan struct{}, 1),
	}

	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
	for {
		select {
		case ci, ok := <-mgr.infos:
			if ok {
				now := time.Now()
				if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) {
					// Cache contains entries and is definitely outdated
					mgr.mgrLog.Trace().Msg("clean destutter cache")
					cache = destutterCache{}
				}
				tsLastEvent = now

				reason, zid := ci.Reason, ci.Zid
				mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier")
				if ignoreUpdate(cache, now, reason, zid) {
					mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored")
					continue
				}

				isStarted := mgr.State() == box.StartStateStarted
				mgr.idxEnqueue(reason, zid)
				if ci.Box == nil {
					ci.Box = mgr







|





|

|







201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
	for {
		select {
		case ci, ok := <-mgr.infos:
			if ok {
				now := time.Now()
				if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) {
					// Cache contains entries and is definitely outdated
					logging.LogTrace(mgr.mgrLogger, "clean destutter cache")
					cache = destutterCache{}
				}
				tsLastEvent = now

				reason, zid := ci.Reason, ci.Zid
				mgr.mgrLogger.Debug("notifier", "reason", reason, "zid", zid)
				if ignoreUpdate(cache, now, reason, zid) {
					logging.LogTrace(mgr.mgrLogger, "notifier ignored", "reason", reason, "zid", zid)
					continue
				}

				isStarted := mgr.State() == box.StartStateStarted
				mgr.idxEnqueue(reason, zid)
				if ci.Box == nil {
					ci.Box = mgr
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
	case box.OnDelete:
		mgr.idxAr.EnqueueZettel(zid)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}







|







258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
	case box.OnDelete:
		mgr.idxAr.EnqueueZettel(zid)
	default:
		mgr.mgrLogger.Error("Unknown notification reason", "reason", reason, "zid", zid)
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337

func (mgr *Manager) waitBoxesAreStarted() {
	const waitTime = 10 * time.Millisecond
	const waitLoop = int(1 * time.Second / waitTime)
	for i := 1; !mgr.allBoxesStarted(); i++ {
		if i%waitLoop == 0 {
			if time.Duration(i)*waitTime > time.Minute {
				mgr.mgrLog.Info().Msg("Waiting for more than one minute to start")
			} else {
				mgr.mgrLog.Trace().Msg("Wait for boxes to start")
			}
		}
		time.Sleep(waitTime)
	}
}

func (mgr *Manager) allBoxesStarted() bool {







|

|







322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338

func (mgr *Manager) waitBoxesAreStarted() {
	const waitTime = 10 * time.Millisecond
	const waitLoop = int(1 * time.Second / waitTime)
	for i := 1; !mgr.allBoxesStarted(); i++ {
		if i%waitLoop == 0 {
			if time.Duration(i)*waitTime > time.Minute {
				mgr.mgrLogger.Info("Waiting for more than one minute to start")
			} else {
				logging.LogTrace(mgr.mgrLogger, "Wait for boxes to start")
			}
		}
		time.Sleep(waitTime)
	}
}

func (mgr *Manager) allBoxesStarted() bool {
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
		}
	}
	mgr.setState(box.StartStateStopped)
}

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

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

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








|
















|









|







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
		}
	}
	mgr.setState(box.StartStateStopped)
}

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

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

// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	mgr.mgrLogger.Debug("ReadStats")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	subStats := make([]box.ManagedBoxStats, len(mgr.boxes))
	for i, p := range mgr.boxes {
		p.ReadStats(&subStats[i])
	}

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
	ms.mxStats.Unlock()
}

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

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

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

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

func (ms *mapStore) dumpDead(w io.Writer) {
	if len(ms.dead) == 0 {
		return
	}
	fmt.Fprintf(w, "==== Dead References\n")
	zids := make([]id.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])
	}
}

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

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







|










|






|


|










|












|






|
|





|

|
|

|







|

|








|

|
|


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")
	ms.dumpIndex(w)
	ms.dumpDead(w)
	dumpStringRefs(w, "Words", "", "", ms.words)
	dumpStringRefs(w, "URLs", "[[", "]]", ms.urls)
}

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

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

func (ms *mapStore) dumpDead(w io.Writer) {
	if len(ms.dead) == 0 {
		return
	}
	_, _ = fmt.Fprintf(w, "==== Dead References\n")
	zids := make([]id.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])
	}
}

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

func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) {
	if len(srefs) == 0 {
		return
	}
	_, _ = fmt.Fprintln(w, "====", title)
	for _, s := range slices.Sorted(maps.Keys(srefs)) {
		_, _ = fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString)
		_, _ = fmt.Fprintln(w, ":", srefs[s])
	}
}
Changes to internal/box/membox/membox.go.
12
13
14
15
16
17
18

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

// Package membox stores zettel volatile in main memory.
package membox

import (
	"context"

	"net/url"
	"sync"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

func init() {
	manager.Register(
		"mem",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &memBox{
				log: kernel.Main.GetLogger(kernel.BoxService).Clone().
					Str("box", "mem").Int("boxnum", int64(cdata.Number)).Child(),
				u:         u,
				cdata:     *cdata,
				maxZettel: box.GetQueryInt(u, "max-zettel", 0, 127, 65535),
				maxBytes:  box.GetQueryInt(u, "max-bytes", 0, 65535, (1024*1024*1024)-1),
			}, nil
		})
}

type memBox struct {
	log       *logger.Logger
	u         *url.URL
	cdata     manager.ConnectData
	maxZettel int
	maxBytes  int
	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]zettel.Zettel
	curBytes  int







>








|









|
<









|







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

// Package membox stores zettel volatile in main memory.
package membox

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

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/box/manager"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

func init() {
	manager.Register(
		"mem",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &memBox{
				logger:    kernel.Main.GetLogger(kernel.BoxService).With("box", "mem", "boxnum", cdata.Number),

				u:         u,
				cdata:     *cdata,
				maxZettel: box.GetQueryInt(u, "max-zettel", 0, 127, 65535),
				maxBytes:  box.GetQueryInt(u, "max-bytes", 0, 65535, (1024*1024*1024)-1),
			}, nil
		})
}

type memBox struct {
	logger    *slog.Logger
	u         *url.URL
	cdata     manager.ConnectData
	maxZettel int
	maxBytes  int
	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]zettel.Zettel
	curBytes  int
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
}

func (mb *memBox) Start(context.Context) error {
	mb.mx.Lock()
	mb.zettel = make(map[id.Zid]zettel.Zettel)
	mb.curBytes = 0
	mb.mx.Unlock()
	mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box")
	return nil
}

func (mb *memBox) Stop(context.Context) {
	mb.mx.Lock()
	mb.zettel = nil
	mb.mx.Unlock()







|







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

func (mb *memBox) Start(context.Context) error {
	mb.mx.Lock()
	mb.zettel = make(map[id.Zid]zettel.Zettel)
	mb.curBytes = 0
	mb.mx.Unlock()
	logging.LogTrace(mb.logger, "Start box", "max-zettel", mb.maxZettel, "max-bytes", mb.maxBytes)
	return nil
}

func (mb *memBox) Stop(context.Context) {
	mb.mx.Lock()
	mb.zettel = nil
	mb.mx.Unlock()
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()

	mb.notifyChanged(zid, box.OnZettel)
	mb.log.Trace().Zid(zid).Msg("CreateZettel")
	return zid, nil
}

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

func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, found := mb.zettel[zid]
	mb.mx.RUnlock()
	return found
}

func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid")
	for zid := range mb.zettel {
		if constraint(zid) {
			handle(zid)
		}
	}
	return nil
}

func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyMeta")
	for zid, zettel := range mb.zettel {
		if constraint(zid) {
			m := zettel.Meta.Clone()
			mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number)
			handle(m)
		}
	}







|











|













|











|







114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()

	mb.notifyChanged(zid, box.OnZettel)
	logging.LogTrace(mb.logger, "CreateZettel", "zid", zid)
	return zid, nil
}

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

func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, found := mb.zettel[zid]
	mb.mx.RUnlock()
	return found
}

func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	logging.LogTrace(mb.logger, "ApplyZid", "entries", len(mb.zettel))
	for zid := range mb.zettel {
		if constraint(zid) {
			handle(zid)
		}
	}
	return nil
}

func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	logging.LogTrace(mb.logger, "ApplyMeta", "entries", len(mb.zettel))
	for zid, zettel := range mb.zettel {
		if constraint(zid) {
			m := zettel.Meta.Clone()
			mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number)
			handle(m)
		}
	}
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid, box.OnZettel)
	mb.log.Trace().Msg("UpdateZettel")
	return nil
}

func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, ok := mb.zettel[zid]
	mb.mx.RUnlock()







|







199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid, box.OnZettel)
	logging.LogTrace(mb.logger, "UpdateZettel")
	return nil
}

func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, ok := mb.zettel[zid]
	mb.mx.RUnlock()
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.ByteSize()
	mb.mx.Unlock()
	mb.notifyChanged(zid, box.OnDelete)
	mb.log.Trace().Msg("DeleteZettel")
	return nil
}

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







|








|

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.ByteSize()
	mb.mx.Unlock()
	mb.notifyChanged(zid, box.OnDelete)
	logging.LogTrace(mb.logger, "DeleteZettel")
	return nil
}

func (mb *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mb.mx.RLock()
	st.Zettel = len(mb.zettel)
	mb.mx.RUnlock()
	logging.LogTrace(mb.logger, "ReadStats", "zettel", st.Zettel)
}
Changes to internal/box/notify/directory.go.
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

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

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/query"
)

type entrySet map[id.Zid]*DirEntry

// DirServiceState signal the internal state of the service.







|










|







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

package notify

import (
	"errors"
	"log/slog"
	"path/filepath"
	"regexp"
	"slices"
	"sync"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/query"
)

type entrySet map[id.Zid]*DirEntry

// DirServiceState signal the internal state of the service.
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
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    box.UpdateNotifier
	mx       sync.RWMutex // protects status, entries
	state    DirServiceState
	entries  entrySet
}

// ErrNoDirectory signals missing directory data.
var ErrNoDirectory = errors.New("unable to retrieve zettel directory information")

// NewDirService creates a new directory service.
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    notify,
		state:    DsCreated,
	}
}

// State the current service state.







|












|


|







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
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	logger   *slog.Logger
	dirPath  string
	notifier Notifier
	infos    box.UpdateNotifier
	mx       sync.RWMutex // protects status, entries
	state    DirServiceState
	entries  entrySet
}

// ErrNoDirectory signals missing directory data.
var ErrNoDirectory = errors.New("unable to retrieve zettel directory information")

// NewDirService creates a new directory service.
func NewDirService(box box.ManagedBox, logger *slog.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService {
	return &DirService{
		box:      box,
		logger:   logger,
		notifier: notifier,
		infos:    notify,
		state:    DsCreated,
	}
}

// State the current service state.
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
	ds.state = DsStopping
	ds.mx.Unlock()
	ds.notifier.Close()
}

func (ds *DirService) logMissingEntry(action string) error {
	err := ErrNoDirectory
	ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information")
	return err
}

// NumDirEntries returns the number of entries in the directory.
func (ds *DirService) NumDirEntries() int {
	ds.mx.RLock()
	defer ds.mx.RUnlock()







|







107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
	ds.state = DsStopping
	ds.mx.Unlock()
	ds.notifier.Close()
}

func (ds *DirService) logMissingEntry(action string) error {
	err := ErrNoDirectory
	ds.logger.Info("Unable to get directory information", "err", err, "action", action)
	return err
}

// NumDirEntries returns the number of entries in the directory.
func (ds *DirService) NumDirEntries() int {
	ds.mx.RLock()
	defer ds.mx.RUnlock()
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
	}
}
func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) {
	ds.mx.RLock()
	state := ds.state
	ds.mx.RUnlock()

	if msg := ds.log.Trace(); msg.Enabled() {
		msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent")
	}
	if state == DsStopping {
		return nil, false
	}

	switch ev.Op {
	case Error:
		newEntries = nil
		if state != DsMissing {
			ds.log.Error().Err(ev.Err).Msg("Notifier confused")
		}
	case Make:
		newEntries = make(entrySet)
	case List:
		if ev.Name == "" {
			zids := getNewZids(newEntries)
			ds.mx.Lock()
			fromMissing := ds.state == DsMissing
			prevEntries := ds.entries
			ds.entries = newEntries
			ds.state = DsWorking
			ds.mx.Unlock()
			ds.onCreateDirectory(zids, prevEntries)
			if fromMissing {
				ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found")
			}
			return nil, true
		}
		if newEntries != nil {
			ds.onUpdateFileEvent(newEntries, ev.Name)
		}
	case Destroy:
		ds.onDestroyDirectory()
		ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing")
		return nil, true
	case Update:
		ds.mx.Lock()
		zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnZettel)
		}
	case Delete:
		ds.mx.Lock()
		zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnDelete)
		}
	default:
		ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
	}
	return newEntries, true
}

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







<
|
<








|














|








|
















|







217
218
219
220
221
222
223

224

225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
	}
}
func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) {
	ds.mx.RLock()
	state := ds.state
	ds.mx.RUnlock()


	logging.LogTrace(ds.logger, "notifyEvent", "state", state, "op", ev.Op, "name", ev.Name)

	if state == DsStopping {
		return nil, false
	}

	switch ev.Op {
	case Error:
		newEntries = nil
		if state != DsMissing {
			ds.logger.Error("Notifier confused", "state", state, logging.Err(ev.Err))
		}
	case Make:
		newEntries = make(entrySet)
	case List:
		if ev.Name == "" {
			zids := getNewZids(newEntries)
			ds.mx.Lock()
			fromMissing := ds.state == DsMissing
			prevEntries := ds.entries
			ds.entries = newEntries
			ds.state = DsWorking
			ds.mx.Unlock()
			ds.onCreateDirectory(zids, prevEntries)
			if fromMissing {
				ds.logger.Info("Zettel directory found", "path", ds.dirPath)
			}
			return nil, true
		}
		if newEntries != nil {
			ds.onUpdateFileEvent(newEntries, ev.Name)
		}
	case Destroy:
		ds.onDestroyDirectory()
		ds.logger.Error("Zettel directory missing", "path", ds.dirPath)
		return nil, true
	case Update:
		ds.mx.Lock()
		zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnZettel)
		}
	case Delete:
		ds.mx.Lock()
		zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnDelete)
		}
	default:
		ds.logger.Error("Unknown zettel notification", "event", ev)
	}
	return newEntries, true
}

func getNewZids(entries entrySet) []id.Zid {
	zids := make([]id.Zid, 0, len(entries))
	for zid := range entries {
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
	zid := seekZid(name)
	if zid == id.Invalid {
		return id.Invalid
	}
	entry := fetchdirEntry(entries, zid)
	dupName1, dupName2 := ds.updateEntry(entry, name)
	if dupName1 != "" {
		ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)")
		if dupName2 != "" {
			ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)")
		}
		return id.Invalid
	}
	return zid
}

func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid {







|

|







342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
	zid := seekZid(name)
	if zid == id.Invalid {
		return id.Invalid
	}
	entry := fetchdirEntry(entries, zid)
	dupName1, dupName2 := ds.updateEntry(entry, name)
	if dupName1 != "" {
		ds.logger.Info("Duplicate content (is ignored)", "name", dupName1)
		if dupName2 != "" {
			ds.logger.Info("Duplicate content (is ignored)", "name", dupName2)
		}
		return id.Invalid
	}
	return zid
}

func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid {
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
loop:
	for _, prevName := range uselessFiles {
		for _, newName := range entry.UselessFiles {
			if prevName == newName {
				continue loop
			}
		}
		ds.log.Info().Str("name", prevName).Msg("Previous duplicate file becomes useful")
	}
}

func (ds *DirService) updateEntry(entry *DirEntry, name string) (string, string) {
	ext := onlyExt(name)
	if !extIsMetaAndContent(entry.ContentExt) {
		if ext == "" {







|







410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
loop:
	for _, prevName := range uselessFiles {
		for _, newName := range entry.UselessFiles {
			if prevName == newName {
				continue loop
			}
		}
		ds.logger.Info("Previous duplicate file becomes useful", "name", prevName)
	}
}

func (ds *DirService) updateEntry(entry *DirEntry, name string) (string, string) {
	ext := onlyExt(name)
	if !extIsMetaAndContent(entry.ContentExt) {
		if ext == "" {
573
574
575
576
577
578
579
580
581
582
583
		return newLen < oldLen
	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) {
	if notify := ds.infos; notify != nil {
		ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange")
		notify(ds.box, zid, reason)
	}
}







|



571
572
573
574
575
576
577
578
579
580
581
		return newLen < oldLen
	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) {
	if notify := ds.infos; notify != nil {
		logging.LogTrace(ds.logger, "notifychange", "zid", zid, "reason", reason)
		notify(ds.box, zid, reason)
	}
}
Changes to internal/box/notify/fsdir.go.
10
11
12
13
14
15
16

17
18
19
20
21

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

58
59
60
61
62
63
64

65
66
67
68
69
70
71
72
73
74
75
76
77
78
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

import (

	"os"
	"path/filepath"
	"strings"

	"github.com/fsnotify/fsnotify"

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

type fsdirNotifier struct {
	log     *logger.Logger
	events  chan Event
	done    chan struct{}
	refresh chan struct{}
	base    *fsnotify.Watcher
	path    string
	fetcher EntryFetcher
	parent  string
}

// NewFSDirNotifier creates a directory based notifier that receives notifications
// from the file system.
func NewFSDirNotifier(log *logger.Logger, path string) (Notifier, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		log.Debug().Err(err).Str("path", path).Msg("Unable to create absolute path")
		return nil, err
	}
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Debug().Err(err).Str("absPath", absPath).Msg("Unable to create watcher")
		return nil, err
	}
	absParentDir := filepath.Dir(absPath)
	errParent := watcher.Add(absParentDir)
	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()
			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 {
		// Not a problem, if container is not available. It might become available later.
		log.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available")
	}

	fsdn := &fsdirNotifier{
		log:     log,
		events:  make(chan Event),
		refresh: make(chan struct{}),
		done:    make(chan struct{}),
		base:    watcher,
		path:    absPath,
		fetcher: newDirPathFetcher(absPath),
		parent:  absParentDir,







>





>
|



|











|


|




|







<
<
<
|
>
|


<
|
|
|
>


|



|







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



56
57
58
59
60

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

import (
	"log/slog"
	"os"
	"path/filepath"
	"strings"

	"github.com/fsnotify/fsnotify"

	"zettelstore.de/z/internal/logging"
)

type fsdirNotifier struct {
	logger  *slog.Logger
	events  chan Event
	done    chan struct{}
	refresh chan struct{}
	base    *fsnotify.Watcher
	path    string
	fetcher EntryFetcher
	parent  string
}

// NewFSDirNotifier creates a directory based notifier that receives notifications
// from the file system.
func NewFSDirNotifier(logger *slog.Logger, path string) (Notifier, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		logger.Debug("Unable to create absolute path", "err", err, "path", path)
		return nil, err
	}
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logger.Debug("Unable to create watcher", "err", err, "absPath", absPath)
		return nil, err
	}
	absParentDir := filepath.Dir(absPath)
	errParent := watcher.Add(absParentDir)
	err = watcher.Add(absPath)
	if errParent != nil {
		if err != nil {



			logger.Error("Unable to access Zettel directory and its parent directory",
				"parentDir", absParentDir, "errParent", errParent, "path", absPath, "err", err)
			_ = watcher.Close()
			return nil, err
		}

		logger.Info("Parent of Zettel directory cannot be supervised",
			"parentDir", absParentDir, "err", errParent)
		logger.Info("Zettelstore might not detect a deletion or movement of the Zettel directory",
			"path", absPath)
	} else if err != nil {
		// Not a problem, if container is not available. It might become available later.
		logger.Info("Zettel directory currently not available", "err", err, "path", absPath)
	}

	fsdn := &fsdirNotifier{
		logger:  logger,
		events:  make(chan Event),
		refresh: make(chan struct{}),
		done:    make(chan struct{}),
		base:    watcher,
		path:    absPath,
		fetcher: newDirPathFetcher(absPath),
		parent:  absParentDir,
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
}

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

func (fsdn *fsdirNotifier) eventLoop() {
	defer 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() {
	}
}

func (fsdn *fsdirNotifier) readAndProcessEvent() bool {
	select {
	case <-fsdn.done:
		fsdn.traceDone(1)
		return false
	default:
	}
	select {
	case <-fsdn.done:
		fsdn.traceDone(2)
		return false
	case <-fsdn.refresh:
		fsdn.log.Trace().Msg("refresh")
		listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done)
	case err, ok := <-fsdn.base.Errors:
		fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors")
		if !ok {
			return false
		}
		select {
		case fsdn.events <- Event{Op: Error, Err: err}:
		case <-fsdn.done:
			fsdn.traceDone(3)
			return false
		}
	case ev, ok := <-fsdn.base.Events:
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event")
		if !ok {
			return false
		}
		if !fsdn.processEvent(&ev) {
			return false
		}
	}
	return true
}

func (fsdn *fsdirNotifier) traceDone(pos int64) {
	fsdn.log.Trace().Int("i", pos).Msg("done with read and process events")
}

func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool {
	if strings.HasPrefix(ev.Name, fsdn.path) {
		if len(ev.Name) == len(fsdn.path) {
			return fsdn.processDirEvent(ev)
		}
		return fsdn.processFileEvent(ev)
	}
	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)
		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
	}

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

	fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed")
	return true
}

func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool {
	if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
		if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() {
			regular := err == nil && fi.Mode().IsRegular()
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Err(err).Bool("regular", regular).Msg("error with file")

			return true
		}
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated")
		return fsdn.sendEvent(Update, filepath.Base(ev.Name))
	}

	if ev.Has(fsnotify.Rename) {
		fi, err := os.Lstat(ev.Name)
		if err != nil {
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted")
			return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
		}
		if fi.Mode().IsRegular() {
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated")
			return fsdn.sendEvent(Update, filepath.Base(ev.Name))
		}
		fsdn.log.Trace().Str("name", ev.Name).Msg("File not regular")
		return true
	}

	if ev.Has(fsnotify.Remove) {
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted")
		return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
	}

	fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed")
	return true
}

func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool {
	select {
	case fsdn.events <- Event{Op: op, Name: filename}:
	case <-fsdn.done:
		fsdn.log.Trace().Msg("done file event processing")
		return false
	}
	return true
}

func (fsdn *fsdirNotifier) Close() {
	close(fsdn.done)
}







|


|



















|
|

|










|











|









|





|
|



|








|



|



|
|


|







|
>


|






|



|


|




|



|







|








86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
}

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

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

	for fsdn.readAndProcessEvent() {
	}
}

func (fsdn *fsdirNotifier) readAndProcessEvent() bool {
	select {
	case <-fsdn.done:
		fsdn.traceDone(1)
		return false
	default:
	}
	select {
	case <-fsdn.done:
		fsdn.traceDone(2)
		return false
	case <-fsdn.refresh:
		logging.LogTrace(fsdn.logger, "refresh")
		listDirElements(fsdn.logger, fsdn.fetcher, fsdn.events, fsdn.done)
	case err, ok := <-fsdn.base.Errors:
		logging.LogTrace(fsdn.logger, "got errors", "err", err, "ok", ok)
		if !ok {
			return false
		}
		select {
		case fsdn.events <- Event{Op: Error, Err: err}:
		case <-fsdn.done:
			fsdn.traceDone(3)
			return false
		}
	case ev, ok := <-fsdn.base.Events:
		logging.LogTrace(fsdn.logger, "file event", "name", ev.Name, "op", ev.Op, "ok", ok)
		if !ok {
			return false
		}
		if !fsdn.processEvent(&ev) {
			return false
		}
	}
	return true
}

func (fsdn *fsdirNotifier) traceDone(pos int64) {
	logging.LogTrace(fsdn.logger, "done with read and process events", "i", pos)
}

func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool {
	if strings.HasPrefix(ev.Name, fsdn.path) {
		if len(ev.Name) == len(fsdn.path) {
			return fsdn.processDirEvent(ev)
		}
		return fsdn.processFileEvent(ev)
	}
	logging.LogTrace(fsdn.logger, "event does not match", "path", fsdn.path, "name", ev.Name, "op", ev.Op)
	return true
}

func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool {
	if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) {
		fsdn.logger.Debug("Directory removed", "name", fsdn.path)
		_ = fsdn.base.Remove(fsdn.path)
		select {
		case fsdn.events <- Event{Op: Destroy}:
		case <-fsdn.done:
			logging.LogTrace(fsdn.logger, "done dir event processing", "i", 1)
			return false
		}
		return true
	}

	if ev.Has(fsnotify.Create) {
		err := fsdn.base.Add(fsdn.path)
		if err != nil {
			fsdn.logger.Error("Unable to add directory", "err", err, "name", fsdn.path)
			select {
			case fsdn.events <- Event{Op: Error, Err: err}:
			case <-fsdn.done:
				logging.LogTrace(fsdn.logger, "done dir event processing", "i", 2)
				return false
			}
		}
		fsdn.logger.Debug("Directory added", "name", fsdn.path)
		return listDirElements(fsdn.logger, fsdn.fetcher, fsdn.events, fsdn.done)
	}

	logging.LogTrace(fsdn.logger, "Directory processed", "name", ev.Name, "op", ev.Op)
	return true
}

func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool {
	if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
		if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() {
			regular := err == nil && fi.Mode().IsRegular()
			logging.LogTrace(fsdn.logger, "error with file",
				"name", ev.Name, "op", ev.Op, logging.Err(err), "regular", regular)
			return true
		}
		logging.LogTrace(fsdn.logger, "File updated", "name", ev.Name, "op", ev.Op)
		return fsdn.sendEvent(Update, filepath.Base(ev.Name))
	}

	if ev.Has(fsnotify.Rename) {
		fi, err := os.Lstat(ev.Name)
		if err != nil {
			logging.LogTrace(fsdn.logger, "File deleted", "name", ev.Name, "op", ev.Op)
			return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
		}
		if fi.Mode().IsRegular() {
			logging.LogTrace(fsdn.logger, "File updated", "name", ev.Name, "op", ev.Op)
			return fsdn.sendEvent(Update, filepath.Base(ev.Name))
		}
		logging.LogTrace(fsdn.logger, "File not regular", "name", ev.Name)
		return true
	}

	if ev.Has(fsnotify.Remove) {
		logging.LogTrace(fsdn.logger, "File deleted", "name", ev.Name, "op", ev.Op)
		return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
	}

	logging.LogTrace(fsdn.logger, "File processed", "name", ev.Name, "op", ev.Op)
	return true
}

func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool {
	select {
	case fsdn.events <- Event{Op: op, Name: filename}:
	case <-fsdn.done:
		logging.LogTrace(fsdn.logger, "done file event processing")
		return false
	}
	return true
}

func (fsdn *fsdirNotifier) Close() {
	close(fsdn.done)
}
Changes to internal/box/notify/helper.go.
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

import (
	"archive/zip"

	"os"

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

// EntryFetcher return a list of (file) names of an directory.
type EntryFetcher interface {
	Fetch() ([]string, error)
}








>


|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

import (
	"archive/zip"
	"log/slog"
	"os"

	"zettelstore.de/z/internal/logging"
)

// EntryFetcher return a list of (file) names of an directory.
type EntryFetcher interface {
	Fetch() ([]string, error)
}

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

	return result, nil
}

// 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:
		return false
	}
	entries, err := fetcher.Fetch()
	if err != nil {
		select {
		case events <- Event{Op: Error, Err: err}:
		case <-done:
			return false
		}
	}
	for _, name := range entries {
		log.Trace().Str("name", name).Msg("File listed")
		select {
		case events <- Event{Op: List, Name: name}:
		case <-done:
			return false
		}
	}








<




>
|



|














|







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

	result := make([]string, 0, len(reader.File))
	for _, f := range reader.File {
		result = append(result, f.Name)
	}
	err = reader.Close()
	return result, err
}

// listDirElements write all files within the directory path as events.
func listDirElements(logger *slog.Logger, fetcher EntryFetcher, events chan<- Event, done <-chan struct{}) bool {
	select {
	case events <- Event{Op: Make}:
	case <-done:
		return false
	}
	entries, err := fetcher.Fetch()
	if err != nil {
		select {
		case events <- Event{Op: Error, Err: err}:
		case <-done:
			return false
		}
	}
	for _, name := range entries {
		logging.LogTrace(logger, "File listed", "name", name)
		select {
		case events <- Event{Op: List, Name: name}:
		case <-done:
			return false
		}
	}

Changes to internal/box/notify/simpledir.go.
10
11
12
13
14
15
16

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

package notify

import (

	"path/filepath"

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

type simpleDirNotifier struct {
	log     *logger.Logger
	events  chan Event
	done    chan struct{}
	refresh chan struct{}
	fetcher EntryFetcher
}

// NewSimpleDirNotifier creates a directory based notifier that will not receive
// any notifications from the operating system.
func NewSimpleDirNotifier(log *logger.Logger, path string) (Notifier, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		return nil, err
	}
	sdn := &simpleDirNotifier{
		log:     log,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newDirPathFetcher(absPath),
	}
	go sdn.eventLoop()
	return sdn, nil
}

// NewSimpleZipNotifier creates a zip-file based notifier that will not receive
// any notifications from the operating system.
func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier {
	sdn := &simpleDirNotifier{
		log:     log,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newZipPathFetcher(zipPath),
	}
	go sdn.eventLoop()
	return sdn
}

func (sdn *simpleDirNotifier) Events() <-chan Event {
	return sdn.events
}

func (sdn *simpleDirNotifier) Refresh() {
	sdn.refresh <- struct{}{}
}

func (sdn *simpleDirNotifier) eventLoop() {
	defer close(sdn.events)
	defer close(sdn.refresh)
	if !listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) {
		return
	}
	for {
		select {
		case <-sdn.done:
			return
		case <-sdn.refresh:
			listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done)
		}
	}
}

func (sdn *simpleDirNotifier) Close() {
	close(sdn.done)
}







>

<
<



|








|





|











|

|




















|







|







10
11
12
13
14
15
16
17
18


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

package notify

import (
	"log/slog"
	"path/filepath"


)

type simpleDirNotifier struct {
	logger  *slog.Logger
	events  chan Event
	done    chan struct{}
	refresh chan struct{}
	fetcher EntryFetcher
}

// NewSimpleDirNotifier creates a directory based notifier that will not receive
// any notifications from the operating system.
func NewSimpleDirNotifier(logger *slog.Logger, path string) (Notifier, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		return nil, err
	}
	sdn := &simpleDirNotifier{
		logger:  logger,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newDirPathFetcher(absPath),
	}
	go sdn.eventLoop()
	return sdn, nil
}

// NewSimpleZipNotifier creates a zip-file based notifier that will not receive
// any notifications from the operating system.
func NewSimpleZipNotifier(logger *slog.Logger, zipPath string) Notifier {
	sdn := &simpleDirNotifier{
		logger:  logger,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newZipPathFetcher(zipPath),
	}
	go sdn.eventLoop()
	return sdn
}

func (sdn *simpleDirNotifier) Events() <-chan Event {
	return sdn.events
}

func (sdn *simpleDirNotifier) Refresh() {
	sdn.refresh <- struct{}{}
}

func (sdn *simpleDirNotifier) eventLoop() {
	defer close(sdn.events)
	defer close(sdn.refresh)
	if !listDirElements(sdn.logger, sdn.fetcher, sdn.events, sdn.done) {
		return
	}
	for {
		select {
		case <-sdn.done:
			return
		case <-sdn.refresh:
			listDirElements(sdn.logger, sdn.fetcher, sdn.events, sdn.done)
		}
	}
}

func (sdn *simpleDirNotifier) Close() {
	close(sdn.done)
}
Changes to internal/encoder/htmlenc.go.
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)
	} 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)







|







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)
	} 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
// 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')
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.







|







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.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
		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)
			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.writeSpaces(4)
		i = j - 1
	}
}

func (v *mdVisitor) visitRegion(rn *ast.RegionNode) {
	if rn.Kind != ast.RegionQuote {







|












|







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)
			continue
		}
		j := i + 1
		for ; j < lc; j++ {
			c := vn.Content[j]
			if c != '\n' && c != '\r' {
				break
			}
		}
		if j >= lcm1 {
			break
		}
		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

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.writeSpaces(regIndent)
		v.b.WriteString(enum)
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				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.WriteString(v.listPrefix)
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				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')
	}
	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)







|





|




















|




|















|







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.WriteLn()
		}
		v.writeSpaces(regIndent)
		v.b.WriteString(enum)
		for j, in := range item {
			if j > 0 {
				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.WriteLn()
		}
		v.b.WriteString(v.listPrefix)
		for j, in := range item {
			if j > 0 {
				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.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
	v.writeReference(ln.Ref, ln.Inlines)
}

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

	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('[')
		ast.Walk(v, &is)
		v.b.WriteStrings("](", ref.String())
		v.b.WriteByte(')')
	} else if isAutoLinkable(ref) {
		v.b.WriteByte('<')
		v.b.WriteString(ref.String())
		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('*')
		ast.Walk(v, &fn.Inlines)
		v.b.WriteByte('*')
	case ast.FormatStrong:
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.writeQuote(fn)
	case ast.FormatMark:







|







|


|

|

|



















|

|







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.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('[')
		ast.Walk(v, &is)
		v.b.WriteStrings("](", ref.String())
		_ = v.b.WriteByte(')')
	} else if isAutoLinkable(ref) {
		_ = v.b.WriteByte('<')
		v.b.WriteString(ref.String())
		_ = 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('*')
		ast.Walk(v, &fn.Inlines)
		_ = v.b.WriteByte('*')
	case ast.FormatStrong:
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.writeQuote(fn)
	case ast.FormatMark:
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('`')
	case ast.LiteralComment: // ignore everything
	default:
		v.b.Write(ln.Content)
	}
}

func (v *mdVisitor) writeSpaces(n int) {
	for range n {
		v.b.WriteByte(' ')
	}
}







|
|
|


|





|


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('`')
	case ast.LiteralComment: // ignore everything
	default:
		_, _ = v.b.Write(ln.Content)
	}
}

func (v *mdVisitor) writeSpaces(n int) {
	for range n {
		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

// 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)
	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')
	}
	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(' ')
		}
		first = false
		buf.WriteString(string(tag.CleanTag()))
	}

}








|














|









|







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)
	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.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.WriteSpace()
		}
		first = false
		buf.WriteString(string(tag.CleanTag()))
	}

}

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')
			ast.Walk(v, &n.Inlines)
		}
		return nil
	case *ast.NestedListNode:
		v.visitNestedList(n)
		return nil
	case *ast.DescriptionListNode:







|







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.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
	case *ast.BLOBNode:
		return nil
	case *ast.TextNode:
		v.visitText(n.Text)
		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteByte('\n')
		} else {
			v.b.WriteByte(' ')
		}
		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(' ')
		}
		// No 'return nil' to write text
	case *ast.LiteralNode:
		if n.Kind != ast.LiteralComment {
			v.b.Write(n.Content)
		}
	}
	return v
}

func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) {
	if vn.Kind == ast.VerbatimComment {
		return
	}
	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')
			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')
	}
	for i, row := range tn.Rows {
		v.writePosChar(i, '\n')
		v.writeRow(row)
	}
}








|

|














|




|






|
|

<

















|











|







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.WriteLn()
		} else {
			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.WriteSpace()
		}
		// No 'return nil' to write text
	case *ast.LiteralNode:
		if n.Kind != ast.LiteralComment {
			_, _ = v.b.Write(n.Content)
		}
	}
	return v
}

func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) {
	if vn.Kind != ast.VerbatimComment {
		_, _ = 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.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.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
}

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

func (v *textVisitor) writePosChar(pos int, ch byte) {
	if pos > 0 {
		v.b.WriteByte(ch)
	}
}







|











|


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.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)
	}
}
Changes to internal/encoder/write.go.
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 {
	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)







|
<
<







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















// Flush returns the collected length and error.
func (w *encWriter) Flush() (int, error) { return w.length, w.err }







|
<
<














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



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) { _, _ = 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
// 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')
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as zmk.







|







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.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
	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.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')
			if lastWasParagraph && !v.inVerse {
				if _, ok := bn.(*ast.ParaNode); ok {
					v.b.WriteByte('\n')
				}
			}
		}
		ast.Walk(v, bn)
		_, lastWasParagraph = bn.(*ast.ParaNode)
	}
}







|

















|


|







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.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.WriteLn()
			if lastWasParagraph && !v.inVerse {
				if _, ok := bn.(*ast.ParaNode); ok {
					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
	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.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')
	saveInVerse := v.inVerse
	v.inVerse = rn.Kind == ast.RegionVerse
	ast.Walk(v, &rn.Blocks)
	v.inVerse = saveInVerse
	v.b.WriteByte('\n')
	v.b.WriteString(kind)
	if len(rn.Inlines) > 0 {
		v.b.WriteByte(' ')
		ast.Walk(v, &rn.Inlines)
	}
}

func (v *zmkVisitor) visitHeading(hn *ast.HeadingNode) {
	const headingSigns = "========= "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])







|
|
|

















|




|


|







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.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.WriteLn()
	saveInVerse := v.inVerse
	v.inVerse = rn.Kind == ast.RegionVerse
	ast.Walk(v, &rn.Blocks)
	v.inVerse = saveInVerse
	v.b.WriteLn()
	v.b.WriteString(kind)
	if len(rn.Inlines) > 0 {
		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
	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.Write(v.prefix)
		v.b.WriteByte(' ')
		for j, in := range item {
			if j > 0 {
				v.b.WriteByte('\n')
				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(' ')
		}
	}
}

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

		for _, b := range descr.Descriptions {
			v.b.WriteString("\n: ")
			for jj, dn := range b {







|

|
|


|













|







|







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.WriteLn()
		}
		_, _ = v.b.Write(v.prefix)
		v.b.WriteSpace()
		for j, in := range item {
			if j > 0 {
				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.WriteSpace()
		}
	}
}

func (v *zmkVisitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	for i, descr := range dn.Descriptions {
		if i > 0 {
			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
	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')
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteByte('\n')
		}
		v.writeTableRow(row, tn.Align)
	}
}

func (v *zmkVisitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
	for pos, cell := range header {







|



|







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.WriteLn()
	}
	for i, row := range tn.Rows {
		if i > 0 {
			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
			v.b.WriteString(alignCode[colAlign])
		}
	}
}

func (v *zmkVisitor) writeTableRow(row ast.TableRow, align []ast.Alignment) {
	for pos, cell := range row {
		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.WriteString("\n@@@\n")
		return
	}
	var sb strings.Builder
	v.textEnc.WriteInlines(&sb, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
}

var escapeSeqs = set.New(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##",
)








|










|




|







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('|')
		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.WriteString("\n@@@\n")
		return
	}
	var sb strings.Builder
	_, _ = v.textEnc.WriteInlines(&sb, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
}

var escapeSeqs = set.New(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##",
)

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

func (v *zmkVisitor) visitLink(ln *ast.LinkNode) {
	v.b.WriteString("[[")
	if len(ln.Inlines) > 0 {
		ast.Walk(v, &ln.Inlines)
		v.b.WriteByte('|')
	}
	if ln.Ref.State == ast.RefStateBased {
		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.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.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(' ')
		ast.Walk(v, &cn.Inlines)
	}
	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('|')
		ast.Walk(v, &mn.Inlines)
	}
	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)
	ast.Walk(v, &fn.Inlines)
	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)
	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('{')
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteByte(' ')
		}
		if k == "-" {
			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('}')
}

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







|








|


|








|







|









|


|






|


|




















|

|

















|
|

















|


|


|





|


|







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.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('|')
	}
	if ln.Ref.State == ast.RefStateBased {
		_ = 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.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.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.WriteSpace()
		ast.Walk(v, &cn.Inlines)
	}
	_ = 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('|')
		ast.Walk(v, &mn.Inlines)
	}
	_ = 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)
	ast.Walk(v, &fn.Inlines)
	_, _ = 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.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('{')
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteSpace()
		}
		if k == "-" {
			_ = 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('}')
}

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
		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)
						result[i] = &ast.VerbatimNode{
							Kind:    ast.VerbatimCode,
							Attrs:   zsx.Attributes{"": classAttr},
							Content: buf.Bytes(),
						}
					}
					return result







|







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)
						result[i] = &ast.VerbatimNode{
							Kind:    ast.VerbatimCode,
							Attrs:   zsx.Attributes{"": classAttr},
							Content: buf.Bytes(),
						}
					}
					return result
Changes to internal/kernel/auth.go.
11
12
13
14
15
16
17

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

37
38
39
40
41
42
43
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package kernel

import (
	"errors"

	"sync"

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

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

type authService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       auth.Manager
	createManager CreateAuthManagerFunc
}

var errAlreadySetOwner = errors.New("changing an existing owner not allowed")
var errAlreadyROMode = errors.New("system in readonly mode cannot change this mode")

func (as *authService) Initialize(logger *logger.Logger) {

	as.logger = logger
	as.descr = descriptionMap{
		AuthOwner: {
			"Owner's zettel id",
			func(val string) (any, error) {
				if owner := as.cur[AuthOwner]; owner != nil && owner != id.Invalid {
					return nil, errAlreadySetOwner







>





<












|
>







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

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package kernel

import (
	"errors"
	"log/slog"
	"sync"

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

	"zettelstore.de/z/internal/auth"

)

type authService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       auth.Manager
	createManager CreateAuthManagerFunc
}

var errAlreadySetOwner = errors.New("changing an existing owner not allowed")
var errAlreadyROMode = errors.New("system in readonly mode cannot change this mode")

func (as *authService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) {
	as.logLevelVar = levelVar
	as.logger = logger
	as.descr = descriptionMap{
		AuthOwner: {
			"Owner's zettel id",
			func(val string) (any, error) {
				if owner := as.cur[AuthOwner]; owner != nil && owner != id.Invalid {
					return nil, errAlreadySetOwner
62
63
64
65
66
67
68
69


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	}
	as.next = interfaceMap{
		AuthOwner:    id.Invalid,
		AuthReadonly: false,
	}
}

func (as *authService) GetLogger() *logger.Logger { return as.logger }



func (as *authService) Start(*Kernel) error {
	as.mxService.Lock()
	defer as.mxService.Unlock()
	readonlyMode := as.GetNextConfig(AuthReadonly).(bool)
	owner := as.GetNextConfig(AuthOwner).(id.Zid)
	authMgr, err := as.createManager(readonlyMode, owner)
	if err != nil {
		as.logger.Error().Err(err).Msg("Unable to create manager")
		return err
	}
	as.logger.Info().Msg("Start Manager")
	as.manager = authMgr
	return nil
}

func (as *authService) IsStarted() bool {
	as.mxService.RLock()
	defer as.mxService.RUnlock()
	return as.manager != nil
}

func (as *authService) Stop(*Kernel) {
	as.logger.Info().Msg("Stop Manager")
	as.mxService.Lock()
	as.manager = nil
	as.mxService.Unlock()
}

func (*authService) GetStatistics() []KeyValue { return nil }







|
>
>








|


|











|






63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
	}
	as.next = interfaceMap{
		AuthOwner:    id.Invalid,
		AuthReadonly: false,
	}
}

func (as *authService) GetLogger() *slog.Logger { return as.logger }
func (as *authService) GetLevel() slog.Level    { return as.logLevelVar.Level() }
func (as *authService) SetLevel(l slog.Level)   { as.logLevelVar.Set(l) }

func (as *authService) Start(*Kernel) error {
	as.mxService.Lock()
	defer as.mxService.Unlock()
	readonlyMode := as.GetNextConfig(AuthReadonly).(bool)
	owner := as.GetNextConfig(AuthOwner).(id.Zid)
	authMgr, err := as.createManager(readonlyMode, owner)
	if err != nil {
		as.logger.Error("Unable to create manager", "err", err)
		return err
	}
	as.logger.Info("Start Manager")
	as.manager = authMgr
	return nil
}

func (as *authService) IsStarted() bool {
	as.mxService.RLock()
	defer as.mxService.RUnlock()
	return as.manager != nil
}

func (as *authService) Stop(*Kernel) {
	as.logger.Info("Stop Manager")
	as.mxService.Lock()
	as.manager = nil
	as.mxService.Unlock()
}

func (*authService) GetStatistics() []KeyValue { return nil }
Changes to internal/kernel/box.go.
14
15
16
17
18
19
20

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

39
40
41
42
43
44
45
package kernel

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

	"net/url"
	"strconv"
	"sync"

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

type boxService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       box.Manager
	createManager CreateBoxManagerFunc
}

var errInvalidDirType = errors.New("invalid directory type")

func (ps *boxService) Initialize(logger *logger.Logger) {

	ps.logger = logger
	ps.descr = descriptionMap{
		BoxDefaultDirType: {
			"Default directory box type",
			ps.noFrozen(func(val string) (any, error) {
				switch val {
				case BoxDirTypeNotify, BoxDirTypeSimple:







>





<











|
>







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

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

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/url"
	"strconv"
	"sync"

	"zettelstore.de/z/internal/box"

)

type boxService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       box.Manager
	createManager CreateBoxManagerFunc
}

var errInvalidDirType = errors.New("invalid directory type")

func (ps *boxService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) {
	ps.logLevelVar = levelVar
	ps.logger = logger
	ps.descr = descriptionMap{
		BoxDefaultDirType: {
			"Default directory box type",
			ps.noFrozen(func(val string) (any, error) {
				switch val {
				case BoxDirTypeNotify, BoxDirTypeSimple:
65
66
67
68
69
70
71
72


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
		},
	}
	ps.next = interfaceMap{
		BoxDefaultDirType: BoxDirTypeNotify,
	}
}

func (ps *boxService) GetLogger() *logger.Logger { return ps.logger }



func (ps *boxService) Start(kern *Kernel) error {
	boxURIs := make([]*url.URL, 0, 4)
	for i := 1; ; i++ {
		u := ps.GetNextConfig(BoxURIs + strconv.Itoa(i))
		if u == nil {
			break
		}
		boxURIs = append(boxURIs, u.(*url.URL))
	}
	ps.mxService.Lock()
	defer ps.mxService.Unlock()
	mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg)
	if err != nil {
		ps.logger.Error().Err(err).Msg("Unable to create manager")
		return err
	}
	ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager")
	if err = mgr.Start(context.Background()); err != nil {
		ps.logger.Error().Err(err).Msg("Unable to start manager")
		return err
	}
	kern.cfg.setBox(mgr)
	ps.manager = mgr
	return nil
}

func (ps *boxService) IsStarted() bool {
	ps.mxService.RLock()
	defer ps.mxService.RUnlock()
	return ps.manager != nil
}

func (ps *boxService) Stop(*Kernel) {
	ps.logger.Info().Msg("Stop Manager")
	ps.mxService.RLock()
	mgr := ps.manager
	ps.mxService.RUnlock()
	mgr.Stop(context.Background())
	ps.mxService.Lock()
	ps.manager = nil
	ps.mxService.Unlock()







|
>
>














|


|

|














|







66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
		},
	}
	ps.next = interfaceMap{
		BoxDefaultDirType: BoxDirTypeNotify,
	}
}

func (ps *boxService) GetLogger() *slog.Logger { return ps.logger }
func (ps *boxService) GetLevel() slog.Level    { return ps.logLevelVar.Level() }
func (ps *boxService) SetLevel(l slog.Level)   { ps.logLevelVar.Set(l) }

func (ps *boxService) Start(kern *Kernel) error {
	boxURIs := make([]*url.URL, 0, 4)
	for i := 1; ; i++ {
		u := ps.GetNextConfig(BoxURIs + strconv.Itoa(i))
		if u == nil {
			break
		}
		boxURIs = append(boxURIs, u.(*url.URL))
	}
	ps.mxService.Lock()
	defer ps.mxService.Unlock()
	mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg)
	if err != nil {
		ps.logger.Error("Unable to create manager", "err", err)
		return err
	}
	ps.logger.Info("Start Manager", "location", mgr.Location())
	if err = mgr.Start(context.Background()); err != nil {
		ps.logger.Error("Unable to start manager", "err", err)
		return err
	}
	kern.cfg.setBox(mgr)
	ps.manager = mgr
	return nil
}

func (ps *boxService) IsStarted() bool {
	ps.mxService.RLock()
	defer ps.mxService.RUnlock()
	return ps.manager != nil
}

func (ps *boxService) Stop(*Kernel) {
	ps.logger.Info("Stop Manager")
	ps.mxService.RLock()
	mgr := ps.manager
	ps.mxService.RUnlock()
	mgr.Stop(context.Background())
	ps.mxService.Lock()
	ps.manager = nil
	ps.mxService.Unlock()
Changes to internal/kernel/cfg.go.
13
14
15
16
17
18
19

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

package kernel

import (
	"context"
	"errors"
	"fmt"

	"strconv"
	"strings"
	"sync"

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

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

type configService struct {
	srvConfig
	mxService sync.RWMutex
	orig      *meta.Meta
	manager   box.Manager







>







|
|
|
|







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

package kernel

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"sync"

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

	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/logging"
)

type configService struct {
	srvConfig
	mxService sync.RWMutex
	orig      *meta.Meta
	manager   box.Manager
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
	keySiteName          = "site-name"
	keyYAMLHeader        = "yaml-header"
	keyZettelFileSyntax  = "zettel-file-syntax"
)

var errUnknownVisibility = errors.New("unknown visibility")

func (cs *configService) Initialize(logger *logger.Logger) {

	cs.logger = logger
	cs.descr = descriptionMap{
		keyDefaultCopyright: {"Default copyright", parseString, true},
		keyDefaultLicense:   {"Default license", parseString, true},
		keyDefaultVisibility: {
			"Default zettel visibility",
			func(val string) (any, error) {







|
>







49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
	keySiteName          = "site-name"
	keyYAMLHeader        = "yaml-header"
	keyZettelFileSyntax  = "zettel-file-syntax"
)

var errUnknownVisibility = errors.New("unknown visibility")

func (cs *configService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) {
	cs.logLevelVar = levelVar
	cs.logger = logger
	cs.descr = descriptionMap{
		keyDefaultCopyright: {"Default copyright", parseString, true},
		keyDefaultLicense:   {"Default license", parseString, true},
		keyDefaultVisibility: {
			"Default zettel visibility",
			func(val string) (any, error) {
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
					return config.ZettelmarkupHTML, nil
				}
				return config.NoHTML, nil
			}),
			true,
		},
		meta.KeyLang:        {"Language", parseString, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},







|







85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
					return config.ZettelmarkupHTML, nil
				}
				return config.NoHTML, nil
			}),
			true,
		},
		meta.KeyLang:        {"Language", parseString, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt, true},
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

128


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176

177
178
179
180



181
182
183
184
185
186
187
188
189
190
191
192
193
194

195
196
197
198



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
		keyDefaultLicense:              "",
		keyDefaultVisibility:           meta.VisibilityLogin,
		keyExpertMode:                  false,
		config.KeyFooterZettel:         id.Invalid,
		config.KeyHomeZettel:           id.ZidDefaultHome,
		ConfigInsecureHTML:             config.NoHTML,
		meta.KeyLang:                   meta.ValueLangEN,
		keyMaxTransclusions:            int64(1024),
		keySiteName:                    "Zettelstore",
		keyYAMLHeader:                  false,
		keyZettelFileSyntax:            nil,
		ConfigSimpleMode:               false,
		config.KeyListsMenuZettel:      id.ZidTOCListsMenu,
		config.KeyShowBackLinks:        "",
		config.KeyShowFolgeLinks:       "",
		config.KeyShowSequelLinks:      "",
		config.KeyShowSubordinateLinks: "",
		config.KeyShowSuccessorLinks:   "",
	}
}

func (cs *configService) GetLogger() *logger.Logger { return cs.logger }



func (cs *configService) Start(*Kernel) error {
	cs.logger.Info().Msg("Start Service")
	data := meta.New(id.ZidConfiguration)
	for _, kv := range cs.GetNextConfigList() {
		data.Set(kv.Key, meta.Value(kv.Value))
	}
	cs.mxService.Lock()
	cs.orig = data
	cs.mxService.Unlock()
	return nil
}

func (cs *configService) IsStarted() bool {
	cs.mxService.RLock()
	defer cs.mxService.RUnlock()
	return cs.orig != nil
}

func (cs *configService) Stop(*Kernel) {
	cs.logger.Info().Msg("Stop Service")
	cs.mxService.Lock()
	cs.orig = nil
	cs.manager = nil
	cs.mxService.Unlock()
}

func (*configService) GetStatistics() []KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {
	cs.mxService.Lock()
	cs.manager = mgr
	cs.mxService.Unlock()
	mgr.RegisterObserver(cs.observe)
	cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ZidConfiguration})
}

func (cs *configService) doUpdate(p box.BaseBox) error {
	z, err := p.GetZettel(context.Background(), id.ZidConfiguration)
	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() {

		if val, ok := m.Get(key); ok {
			cs.SetConfig(key, string(val))
		} else if defVal, defFound := cs.orig.Get(key); defFound {
			cs.SetConfig(key, string(defVal))



		}
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ZidConfiguration {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() {
			cs.mxService.RLock()
			mgr := cs.manager
			cs.mxService.RUnlock()

			if mgr != nil {
				cs.doUpdate(mgr)
			} else {
				cs.doUpdate(ci.Box)



			}
		}()
	}
}

// --- config.Config

func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
	if m != nil {
		if val, found := m.Get(key); found {
			return string(val)
		}
	}
	if user := server.GetUser(ctx); user != nil {
		if val, found := user.Get(key); found {
			return string(val)
		}
	}
	result := cs.GetCurConfig(key)
	if result == nil {
		return ""







|












>
|
>
>


|

















|




















|






>

|

|
>
>
>




|




|




>

|

|
>
>
>













|







110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
		keyDefaultLicense:              "",
		keyDefaultVisibility:           meta.VisibilityLogin,
		keyExpertMode:                  false,
		config.KeyFooterZettel:         id.Invalid,
		config.KeyHomeZettel:           id.ZidDefaultHome,
		ConfigInsecureHTML:             config.NoHTML,
		meta.KeyLang:                   meta.ValueLangEN,
		keyMaxTransclusions:            1024,
		keySiteName:                    "Zettelstore",
		keyYAMLHeader:                  false,
		keyZettelFileSyntax:            nil,
		ConfigSimpleMode:               false,
		config.KeyListsMenuZettel:      id.ZidTOCListsMenu,
		config.KeyShowBackLinks:        "",
		config.KeyShowFolgeLinks:       "",
		config.KeyShowSequelLinks:      "",
		config.KeyShowSubordinateLinks: "",
		config.KeyShowSuccessorLinks:   "",
	}
}

func (cs *configService) GetLogger() *slog.Logger { return cs.logger }
func (cs *configService) GetLevel() slog.Level    { return cs.logLevelVar.Level() }
func (cs *configService) SetLevel(l slog.Level)   { cs.logLevelVar.Set(l) }

func (cs *configService) Start(*Kernel) error {
	cs.logger.Info("Start Service")
	data := meta.New(id.ZidConfiguration)
	for _, kv := range cs.GetNextConfigList() {
		data.Set(kv.Key, meta.Value(kv.Value))
	}
	cs.mxService.Lock()
	cs.orig = data
	cs.mxService.Unlock()
	return nil
}

func (cs *configService) IsStarted() bool {
	cs.mxService.RLock()
	defer cs.mxService.RUnlock()
	return cs.orig != nil
}

func (cs *configService) Stop(*Kernel) {
	cs.logger.Info("Stop Service")
	cs.mxService.Lock()
	cs.orig = nil
	cs.manager = nil
	cs.mxService.Unlock()
}

func (*configService) GetStatistics() []KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {
	cs.mxService.Lock()
	cs.manager = mgr
	cs.mxService.Unlock()
	mgr.RegisterObserver(cs.observe)
	cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ZidConfiguration})
}

func (cs *configService) doUpdate(p box.BaseBox) error {
	z, err := p.GetZettel(context.Background(), id.ZidConfiguration)
	logging.LogTrace(cs.logger, "got config meta", logging.Err(err))
	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 {
			setErr = cs.SetConfig(key, string(val))
		} else if defVal, defFound := cs.orig.Get(key); defFound {
			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 err
}

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

// --- config.Config

func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
	if m != nil {
		if val, found := m.Get(key); found {
			return string(val)
		}
	}
	if user := user.GetCurrentUser(ctx); user != nil {
		if val, found := user.Get(key); found {
			return string(val)
		}
	}
	result := cs.GetCurConfig(key)
	if result == nil {
		return ""
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
}

// GetSiteName returns the current value of the "site-name" key.
func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) }

// GetMaxTransclusions return the maximum number of indirect transclusions.
func (cs *configService) GetMaxTransclusions() int {
	return int(cs.GetCurConfig(keyMaxTransclusions).(int64))
}

// GetYAMLHeader returns the current value of the "yaml-header" key.
func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) }

// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
func (cs *configService) GetZettelFileSyntax() []meta.Value {







|







290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
}

// GetSiteName returns the current value of the "site-name" key.
func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) }

// GetMaxTransclusions return the maximum number of indirect transclusions.
func (cs *configService) GetMaxTransclusions() int {
	return int(cs.GetCurConfig(keyMaxTransclusions).(int))
}

// GetYAMLHeader returns the current value of the "yaml-header" key.
func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) }

// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
func (cs *configService) GetZettelFileSyntax() []meta.Value {
Changes to internal/kernel/cmd.go.
12
13
14
15
16
17
18

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

package kernel

import (
	"fmt"
	"io"

	"maps"
	"os"
	"runtime/metrics"
	"slices"
	"strconv"
	"strings"

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

type cmdSession struct {
	w        io.Writer
	kern     *Kernel
	echo     bool







>







|







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

package kernel

import (
	"fmt"
	"io"
	"log/slog"
	"maps"
	"os"
	"runtime/metrics"
	"slices"
	"strconv"
	"strings"

	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/strfun"
)

type cmdSession struct {
	w        io.Writer
	kern     *Kernel
	echo     bool
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])
		for _, arg := range args[1:] {
			io.WriteString(sess.w, " ")
			io.WriteString(sess.w, arg)
		}
	}
	sess.w.Write(sess.eol)
}

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

func (sess *cmdSession) printTable(table [][]string) {







|

|
|


|







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
	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])
		for _, arg := range args[1:] {
			_, _ = io.WriteString(sess.w, " ")
			_, _ = io.WriteString(sess.w, arg)
		}
	}
	_, _ = 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
		}
	}
	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)
		prefix = delim
		io.WriteString(sess.w, strfun.JustifyLeft(column, maxLen[colno], pad))
	}
	sess.w.Write(sess.eol)
}

func splitLine(line string) (string, []string) {
	s := strings.Fields(line)
	if len(s) == 0 {
		return "", nil
	}







|

|

|







107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
		}
	}
	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)
		prefix = delim
		_, _ = io.WriteString(sess.w, strfun.JustifyLeft(column, maxLen[colno], pad))
	}
	_, _ = sess.w.Write(sess.eol)
}

func splitLine(line string) (string, []string) {
	s := strings.Fields(line)
	if len(s) == 0 {
		return "", nil
	}
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
	srvD, found := getService(sess, args[0])
	if !found {
		return true
	}
	key := args[1]
	newValue := strings.Join(args[2:], " ")
	if err := srvD.srv.SetConfig(key, newValue); err == nil {
		sess.kern.logger.Mandatory().Str("key", key).Str("value", newValue).Msg("Update system configuration")
	} else {
		sess.println("Unable to set key", args[1], "to value", newValue, "because:", err.Error())
	}
	return true
}

func cmdServices(sess *cmdSession, _ string, _ []string) bool {







|







287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
	srvD, found := getService(sess, args[0])
	if !found {
		return true
	}
	key := args[1]
	newValue := strings.Join(args[2:], " ")
	if err := srvD.srv.SetConfig(key, newValue); err == nil {
		logging.LogMandatory(sess.kern.logger, "Update system configuration", "key", key, "value", newValue)
	} else {
		sess.println("Unable to set key", args[1], "to value", newValue, "because:", err.Error())
	}
	return true
}

func cmdServices(sess *cmdSession, _ string, _ []string) bool {
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
	return true
}

func cmdLogLevel(sess *cmdSession, _ string, args []string) bool {
	kern := sess.kern
	if len(args) == 0 {
		// Write log levels
		level := kern.logger.Level()
		table := [][]string{
			{"Service", "Level", "Name"},
			{"kernel", strconv.Itoa(int(level)), level.String()},
		}
		for _, name := range sortedServiceNames(sess) {
			level = kern.srvNames[name].srv.GetLogger().Level()
			table = append(table, []string{name, strconv.Itoa(int(level)), level.String()})
		}
		sess.printTable(table)
		return true
	}
	var l *logger.Logger
	name := args[0]
	if name == "kernel" {
		l = kern.logger
	} else {
		srvD, ok := getService(sess, name)
		if !ok {
			return true
		}
		l = srvD.srv.GetLogger()
	}

	if len(args) == 1 {
		level := l.Level()
		sess.println(strconv.Itoa(int(level)), level.String())
		return true
	}

	level := args[1]

	uval, err := strconv.ParseUint(level, 10, 8)
	lv := logger.Level(uval)
	if err != nil || !lv.IsValid() {
		lv = logger.ParseLevel(level)
	}
	if !lv.IsValid() {

		sess.println("Invalid level:", level)
		return true
	}

	kern.logger.Mandatory().Str("name", name).Str("level", lv.String()).Msg("Update log level")
	l.SetLevel(lv)
	return true
}

func lookupService(sess *cmdSession, cmd string, args []string) (Service, bool) {
	if len(args) == 0 {
		sess.usage(cmd, "SERVICE")
		return 0, false







|


|


|
|




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

|
|



|
>
|
|
|
|

<
>
|


>
|
|







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

func cmdLogLevel(sess *cmdSession, _ string, args []string) bool {
	kern := sess.kern
	if len(args) == 0 {
		// Write log levels
		level := kern.GetKernelLogLevel()
		table := [][]string{
			{"Service", "Level", "Name"},
			{"(kernel)", strconv.Itoa(int(level)), logging.LevelString(level)},
		}
		for _, name := range sortedServiceNames(sess) {
			level = kern.srvNames[name].srv.GetLevel()
			table = append(table, []string{name, strconv.Itoa(int(level.Level())), logging.LevelString(level)})
		}
		sess.printTable(table)
		return true
	}





	srvD, ok := getService(sess, args[0])
	if !ok {
		return true
	}
	srv := srvD.srv


	if len(args) == 1 {
		lvl := srv.GetLevel()
		sess.println(strconv.Itoa(int(lvl)), lvl.String())
		return true
	}

	levelString := args[1]
	var lvl slog.Level
	if val, err := strconv.ParseInt(levelString, 10, 8); err == nil {
		lvl = slog.Level(val)
	} else {
		lvl = logging.ParseLevel(levelString)
	}

	if lvl <= logging.LevelMissing || logging.LevelMandatory < lvl {
		sess.println("Invalid level: ", levelString)
		return true
	}
	logging.LogMandatory(kern.logger,
		"Update log level", "name", args[0], "level", logging.LevelString(lvl))
	srv.SetLevel(lvl)
	return true
}

func lookupService(sess *cmdSession, cmd string, args []string) (Service, bool) {
	if len(args) == 0 {
		sess.usage(cmd, "SERVICE")
		return 0, false
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
	} else {
		fileName = args[1]
	}
	kern := sess.kern
	if err := kern.doStartProfiling(profileName, fileName); err != nil {
		sess.println("Error:", err.Error())
	} else {
		kern.logger.Mandatory().Str("profile", profileName).Str("file", fileName).Msg("Start profiling")
	}
	return true
}
func cmdEndProfile(sess *cmdSession, _ string, _ []string) bool {
	kern := sess.kern
	err := kern.doStopProfiling()
	if err != nil {
		sess.println("Error:", err.Error())
	}
	kern.logger.Mandatory().Err(err).Msg("Stop profiling")
	return true
}

func cmdMetrics(sess *cmdSession, _ string, _ []string) bool {
	var samples []metrics.Sample
	all := metrics.All()
	for _, d := range all {







|









|







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
	} else {
		fileName = args[1]
	}
	kern := sess.kern
	if err := kern.doStartProfiling(profileName, fileName); err != nil {
		sess.println("Error:", err.Error())
	} else {
		logging.LogMandatory(sess.kern.logger, "Start profiling", "profile", profileName, "file", fileName)
	}
	return true
}
func cmdEndProfile(sess *cmdSession, _ string, _ []string) bool {
	kern := sess.kern
	err := kern.doStopProfiling()
	if err != nil {
		sess.println("Error:", err.Error())
	}
	logging.LogMandatory(sess.kern.logger, "Stop profiling", logging.Err(err))
	return true
}

func cmdMetrics(sess *cmdSession, _ string, _ []string) bool {
	var samples []metrics.Sample
	all := metrics.All()
	for _, d := range all {
499
500
501
502
503
504
505
506
507


508
509
510
511
512
513
514
func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool {
	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()


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







|
|
>
>







497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool {
	sess.kern.dumpIndex(sess.w)
	return true
}

func cmdRefresh(sess *cmdSession, _ string, _ []string) bool {
	kern := sess.kern
	logging.LogMandatory(sess.kern.logger, "Refresh")
	if err := kern.box.Refresh(); err != nil {
		logging.LogMandatory(sess.kern.logger, "refresh", "err", err)
	}
	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.
12
13
14
15
16
17
18

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

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

package kernel

import (
	"errors"
	"fmt"

	"maps"
	"slices"
	"strconv"
	"strings"
	"sync"

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

type parseFunc func(string) (any, error)
type configDescription struct {
	text    string
	parse   parseFunc
	canList bool
}
type descriptionMap map[string]configDescription
type interfaceMap map[string]any

func (m interfaceMap) Clone() interfaceMap { return maps.Clone(m) }

type srvConfig struct {

	logger   *logger.Logger
	mxConfig sync.RWMutex
	frozen   bool
	descr    descriptionMap
	cur      interfaceMap
	next     interfaceMap
}

func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription {
	cfg.mxConfig.RLock()
	defer cfg.mxConfig.RUnlock()
	keys := slices.Sorted(maps.Keys(cfg.descr))
	result := make([]serviceConfigDescription, 0, len(keys))
	for _, k := range keys {
		text := cfg.descr[k].text
		if strings.HasSuffix(k, "-") {
			text = text + " (list)"
		}
		result = append(result, serviceConfigDescription{Key: k, Descr: text})
	}
	return result
}

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

func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc {
	return func(val string) (any, error) {
		if cfg.frozen {
			return nil, errAlreadyFrozen
		}
		return parse(val)







>







<














>
|
|
|
|
|
|

















|







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

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

package kernel

import (
	"errors"
	"fmt"
	"log/slog"
	"maps"
	"slices"
	"strconv"
	"strings"
	"sync"

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

)

type parseFunc func(string) (any, error)
type configDescription struct {
	text    string
	parse   parseFunc
	canList bool
}
type descriptionMap map[string]configDescription
type interfaceMap map[string]any

func (m interfaceMap) Clone() interfaceMap { return maps.Clone(m) }

type srvConfig struct {
	logLevelVar *slog.LevelVar
	logger      *slog.Logger
	mxConfig    sync.RWMutex
	frozen      bool
	descr       descriptionMap
	cur         interfaceMap
	next        interfaceMap
}

func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription {
	cfg.mxConfig.RLock()
	defer cfg.mxConfig.RUnlock()
	keys := slices.Sorted(maps.Keys(cfg.descr))
	result := make([]serviceConfigDescription, 0, len(keys))
	for _, k := range keys {
		text := cfg.descr[k].text
		if strings.HasSuffix(k, "-") {
			text = text + " (list)"
		}
		result = append(result, serviceConfigDescription{Key: k, Descr: text})
	}
	return result
}

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)
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244

func (cfg *srvConfig) SwitchNextToCur() {
	cfg.mxConfig.Lock()
	defer cfg.mxConfig.Unlock()
	cfg.cur = cfg.next.Clone()
}

func parseString(val string) (any, error) { return val, nil }

var errNoBoolean = errors.New("no boolean value")

func parseBool(val string) (any, error) {
	if val == "" {
		return false, errNoBoolean
	}
	switch val[0] {
	case '0', 'f', 'F', 'n', 'N':
		return false, nil
	}
	return true, nil
}

func parseInt64(val string) (any, error) {
	u64, err := strconv.ParseInt(val, 10, 64)
	if err == nil {
		return u64, nil
	}
	return nil, err
}

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

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







<
<













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




204
205
206
207
208
209
210


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225





226
227







228
229
230
231

func (cfg *srvConfig) SwitchNextToCur() {
	cfg.mxConfig.Lock()
	defer cfg.mxConfig.Unlock()
	cfg.cur = cfg.next.Clone()
}



var errNoBoolean = errors.New("no boolean value")

func parseBool(val string) (any, error) {
	if val == "" {
		return false, errNoBoolean
	}
	switch val[0] {
	case '0', 'f', 'F', 'n', 'N':
		return false, nil
	}
	return true, nil
}

func parseString(val string) (any, error) { return val, nil }
func parseInt(val string) (any, error)    { return strconv.Atoi(val) }





func parseInt64(val string) (any, error)  { return strconv.ParseInt(val, 10, 64) }
func parseZid(val string) (any, error)    { return id.Parse(val) }







func parseInvalidZid(val string) (any, error) {
	zid, _ := id.Parse(val)
	return zid, nil
}
Changes to internal/kernel/core.go.
11
12
13
14
15
16
17

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

47
48
49
50
51
52
53
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package kernel

import (
	"fmt"

	"maps"
	"net"
	"os"
	"runtime"
	"slices"
	"sync"
	"time"

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

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

type coreService struct {
	srvConfig
	started bool

	mxRecover  sync.RWMutex
	mapRecover map[string]recoverInfo
}
type recoverInfo struct {
	count uint64
	ts    time.Time
	info  any
	stack []byte
}

func (cs *coreService) Initialize(logger *logger.Logger) {

	cs.logger = logger
	cs.mapRecover = make(map[string]recoverInfo)
	cs.descr = descriptionMap{
		CoreDebug:     {"Debug mode", parseBool, false},
		CoreGoArch:    {"Go processor architecture", nil, false},
		CoreGoOS:      {"Go Operating System", nil, false},
		CoreGoVersion: {"Go Version", nil, false},







>










<

















|
>







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

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

package kernel

import (
	"fmt"
	"log/slog"
	"maps"
	"net"
	"os"
	"runtime"
	"slices"
	"sync"
	"time"

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


	"zettelstore.de/z/strfun"
)

type coreService struct {
	srvConfig
	started bool

	mxRecover  sync.RWMutex
	mapRecover map[string]recoverInfo
}
type recoverInfo struct {
	count uint64
	ts    time.Time
	info  any
	stack []byte
}

func (cs *coreService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) {
	cs.logLevelVar = levelVar
	cs.logger = logger
	cs.mapRecover = make(map[string]recoverInfo)
	cs.descr = descriptionMap{
		CoreDebug:     {"Debug mode", parseBool, false},
		CoreGoArch:    {"Go processor architecture", nil, false},
		CoreGoOS:      {"Go Operating System", nil, false},
		CoreGoVersion: {"Go Version", nil, false},
89
90
91
92
93
94
95
96


97
98
99
100
101
102
103
		CoreVerbose:   false,
	}
	if hn, err := os.Hostname(); err == nil {
		cs.next[CoreHostname] = hn
	}
}

func (cs *coreService) GetLogger() *logger.Logger { return cs.logger }



func (cs *coreService) Start(*Kernel) error {
	cs.started = true
	return nil
}
func (cs *coreService) IsStarted() bool { return cs.started }
func (cs *coreService) Stop(*Kernel) {







|
>
>







90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
		CoreVerbose:   false,
	}
	if hn, err := os.Hostname(); err == nil {
		cs.next[CoreHostname] = hn
	}
}

func (cs *coreService) GetLogger() *slog.Logger { return cs.logger }
func (cs *coreService) GetLevel() slog.Level    { return cs.logLevelVar.Level() }
func (cs *coreService) SetLevel(l slog.Level)   { cs.logLevelVar.Set(l) }

func (cs *coreService) Start(*Kernel) error {
	cs.started = true
	return nil
}
func (cs *coreService) IsStarted() bool { return cs.started }
func (cs *coreService) Stop(*Kernel) {
Changes to internal/kernel/kernel.go.
14
15
16
17
18
19
20

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


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

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


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



111
112
113
114
115
116
117
118
119
120
// Package kernel provides the main kernel service.
package kernel

import (
	"errors"
	"fmt"
	"io"

	"net"
	"net/url"
	"os"
	"os/signal"
	"runtime"
	"runtime/debug"
	"runtime/pprof"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/web/server"
)

// Main references the main kernel.
var Main *Kernel

// Kernel is the main internal kernel.
type Kernel struct {


	logWriter *kernelLogWriter
	logger    *logger.Logger
	wg        sync.WaitGroup
	mx        sync.RWMutex
	interrupt chan os.Signal

	profileName string
	fileName    string
	profileFile *os.File
	profile     *pprof.Profile

	self kernelService
	core coreService
	cfg  configService
	auth authService
	box  boxService
	web  webService

	srvs     map[Service]serviceDescr
	srvNames map[string]serviceData
	depStart serviceDependency
	depStop  serviceDependency // reverse of depStart
}

type serviceDescr struct {
	srv      service
	name     string
	logLevel logger.Level

}
type serviceData struct {
	srv    service
	srvnum Service
}
type serviceDependency map[Service][]Service

// create a new kernel.
func init() {
	Main = createKernel()
}

// create a new
func createKernel() *Kernel {
	lw := newKernelLogWriter(8192)
	kern := &Kernel{
		logWriter: lw,
		logger:    logger.New(lw, "").SetLevel(defaultNormalLogLevel),
		interrupt: make(chan os.Signal, 5),
	}


	kern.self.kernel = kern
	kern.srvs = map[Service]serviceDescr{
		KernelService: {&kern.self, "kernel", defaultNormalLogLevel},
		CoreService:   {&kern.core, "core", defaultNormalLogLevel},
		ConfigService: {&kern.cfg, "config", defaultNormalLogLevel},
		AuthService:   {&kern.auth, "auth", defaultNormalLogLevel},
		BoxService:    {&kern.box, "box", defaultNormalLogLevel},
		WebService:    {&kern.web, "web", defaultNormalLogLevel},
	}
	kern.srvNames = make(map[string]serviceData, len(kern.srvs))
	for key, srvD := range kern.srvs {
		if _, ok := kern.srvNames[srvD.name]; ok {
			kern.logger.Error().Str("service", srvD.name).Msg("Service data already set, ignore")
		}
		kern.srvNames[srvD.name] = serviceData{srvD.srv, key}



		l := logger.New(lw, strings.ToUpper(srvD.name)).SetLevel(srvD.logLevel)
		kern.logger.Debug().Str("service", srvD.name).Msg("Initialize")
		srvD.srv.Initialize(l)
	}
	kern.depStart = serviceDependency{
		KernelService: nil,
		CoreService:   {KernelService},
		ConfigService: {CoreService},
		AuthService:   {CoreService},
		BoxService:    {CoreService, ConfigService, AuthService},







>


















|








>
>
|
<
|
|
|













|






|
|
|
>














|

|
<
|

>
>

|
|
|
|
|
|
|




|


>
>
>
|
|
|







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

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// Package kernel provides the main kernel service.
package kernel

import (
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/url"
	"os"
	"os/signal"
	"runtime"
	"runtime/debug"
	"runtime/pprof"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/web/server"
)

// Main references the main kernel.
var Main *Kernel

// Kernel is the main internal kernel.
type Kernel struct {
	logLevelVar slog.LevelVar
	logger      *slog.Logger
	dlogWriter  *kernelLogWriter

	wg          sync.WaitGroup
	mx          sync.RWMutex
	interrupt   chan os.Signal

	profileName string
	fileName    string
	profileFile *os.File
	profile     *pprof.Profile

	self kernelService
	core coreService
	cfg  configService
	auth authService
	box  boxService
	web  webService

	srvs     map[Service]*serviceDescr
	srvNames map[string]serviceData
	depStart serviceDependency
	depStop  serviceDependency // reverse of depStart
}

type serviceDescr struct {
	srv         service
	name        string
	logLevel    slog.Level
	logLevelVar slog.LevelVar
}
type serviceData struct {
	srv    service
	srvnum Service
}
type serviceDependency map[Service][]Service

// create a new kernel.
func init() {
	Main = createKernel()
}

// create a new
func createKernel() *Kernel {
	lw := newKernelLogWriter(os.Stdout, 8192)
	kern := &Kernel{
		dlogWriter: lw,

		interrupt:  make(chan os.Signal, 5),
	}
	kern.logLevelVar.Set(defaultNormalLogLevel)
	kern.logger = slog.New(newKernelLogHandler(lw, &kern.logLevelVar))
	kern.self.kernel = kern
	kern.srvs = map[Service]*serviceDescr{
		KernelService: {srv: &kern.self, name: "kernel", logLevel: defaultNormalLogLevel},
		CoreService:   {srv: &kern.core, name: "core", logLevel: defaultNormalLogLevel},
		ConfigService: {srv: &kern.cfg, name: "config", logLevel: defaultNormalLogLevel},
		AuthService:   {srv: &kern.auth, name: "auth", logLevel: defaultNormalLogLevel},
		BoxService:    {srv: &kern.box, name: "box", logLevel: defaultNormalLogLevel},
		WebService:    {srv: &kern.web, name: "web", logLevel: defaultNormalLogLevel},
	}
	kern.srvNames = make(map[string]serviceData, len(kern.srvs))
	for key, srvD := range kern.srvs {
		if _, ok := kern.srvNames[srvD.name]; ok {
			kern.logger.Error("Service data already set, ignored", "service", srvD.name)
		}
		kern.srvNames[srvD.name] = serviceData{srvD.srv, key}

		srvD.logLevelVar.Set(srvD.logLevel)
		srvLogger := slog.New(newKernelLogHandler(lw, &srvD.logLevelVar)).With(
			"system", strings.ToUpper(srvD.name))
		kern.logger.Debug("Initialize", "service", srvD.name)
		srvD.srv.Initialize(&srvD.logLevelVar, srvLogger)
	}
	kern.depStart = serviceDependency{
		KernelService: nil,
		CoreService:   {KernelService},
		ConfigService: {CoreService},
		AuthService:   {CoreService},
		BoxService:    {CoreService, ConfigService, AuthService},
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
)

// Constants for web service keys.
const (
	WebAssetDir          = "asset-dir"
	WebBaseURL           = "base-url"
	WebListenAddress     = "listen"


	WebPersistentCookie  = "persistent"
	WebProfiling         = "profiling"
	WebMaxRequestSize    = "max-request-size"
	WebSecureCookie      = "secure"

	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)

// KeyDescrValue is a triple of config data.
type KeyDescrValue struct{ Key, Descr, Value string }

// KeyValue is a pair of key and value.
type KeyValue struct{ Key, Value string }

// LogEntry stores values of one log line written by a logger.Logger
type LogEntry struct {
	Level   logger.Level
	TS      time.Time
	Prefix  string
	Message string

}

// CreateAuthManagerFunc is called to create a new auth manager.
type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error)

// CreateBoxManagerFunc is called to create a new box manager.
type CreateBoxManagerFunc func(







>
>




>











|

|



>







209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
)

// 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"
	WebSxMaxNesting      = "sx-max-nesting"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)

// KeyDescrValue is a triple of config data.
type KeyDescrValue struct{ Key, Descr, Value string }

// KeyValue is a pair of key and value.
type KeyValue struct{ Key, Value string }

// LogEntry stores values of one log line written by a logger.
type LogEntry struct {
	Level   slog.Level
	TS      time.Time
	Prefix  string
	Message string
	Details string
}

// CreateAuthManagerFunc is called to create a new auth manager.
type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error)

// CreateBoxManagerFunc is called to create a new box manager.
type CreateBoxManagerFunc func(
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351


352
353
354
355
356
357
358
359
360

361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
	webServer server.Server,
	boxManager box.Manager,
	authManager auth.Manager,
	rtConfig config.Config,
) error

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

// Start the service.
func (kern *Kernel) Start(headline, lineServer bool, configFilename string) {
	for _, srvD := range kern.srvs {
		srvD.srv.Freeze()
	}
	if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) {
		kern.SetLogLevel(defaultSimpleLogLevel.String())
	}
	kern.wg.Add(1)
	signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM)
	go func() {
		// Wait for interrupt.
		sig := <-kern.interrupt
		if strSig := sig.String(); strSig != "" {
			kern.logger.Info().Str("signal", strSig).Msg("Shut down Zettelstore")
		}
		kern.doShutdown()
		kern.wg.Done()
	}()

	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),
			kern.core.GetCurConfig(CoreGoOS),
			kern.core.GetCurConfig(CoreGoArch),
		))
		logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)")
		if configFilename != "" {
			logger.Mandatory().Str("filename", configFilename).Msg("Configuration file found")
		} else {
			logger.Mandatory().Msg("No configuration file found / used")
		}
		if kern.core.GetCurConfig(CoreDebug).(bool) {
			logger.Info().Msg("----------------------------------------")
			logger.Info().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION")
			logger.Info().Msg("----------------------------------------")
		}
		if kern.auth.GetCurConfig(AuthReadonly).(bool) {
			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)
		}
	}
}

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

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

// Shutdown the service. Waits for all concurrent activities to stop.
func (kern *Kernel) Shutdown(silent bool) {
	kern.logger.Trace().Msg("Shutdown")
	kern.interrupt <- &shutdownSignal{silent: silent}
}

type shutdownSignal struct{ silent bool }

func (s *shutdownSignal) String() string {
	if s.silent {
		return ""
	}
	return "shutdown"
}
func (*shutdownSignal) Signal() { /* Just a signal */ }

// --- Log operation ---------------------------------------------------------

// GetKernelLogger returns the kernel logger.
func (kern *Kernel) GetKernelLogger() *logger.Logger {
	return kern.logger
}



// SetLogLevel sets the logging level for logger maintained by the kernel.
//
// Its syntax is: (SERVICE ":")? LEVEL (";" (SERICE ":")? LEVEL)*.
func (kern *Kernel) SetLogLevel(logLevel string) {
	defaultLevel, srvLevel := kern.parseLogLevel(logLevel)

	kern.mx.RLock()
	defer kern.mx.RUnlock()

	for srvN, srvD := range kern.srvs {
		if lvl, found := srvLevel[srvN]; found {
			srvD.srv.GetLogger().SetLevel(lvl)
		} else if defaultLevel != logger.NoLevel {
			srvD.srv.GetLogger().SetLevel(defaultLevel)
		}
	}
}

func (kern *Kernel) parseLogLevel(logLevel string) (logger.Level, map[Service]logger.Level) {
	defaultLevel := logger.NoLevel
	srvLevel := map[Service]logger.Level{}
	for spec := range strings.SplitSeq(logLevel, ";") {
		vals := cleanLogSpec(strings.Split(spec, ":"))
		switch len(vals) {
		case 0:
		case 1:
			if lvl := logger.ParseLevel(vals[0]); lvl.IsValid() {
				defaultLevel = lvl
			}
		default:
			serviceText, levelText := vals[0], vals[1]
			if srv, found := kern.srvNames[serviceText]; found {
				if lvl := logger.ParseLevel(levelText); lvl.IsValid() {
					srvLevel[srv.srvnum] = lvl
				}
			}
		}
	}
	return defaultLevel, srvLevel
}







|
|





|
|
|








|







|





|


|







|

|

|


|
|
|


|






|











|






|
















|
<
|
>
>



|





>


|
|
|




|
|
|





|





|







255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360

361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
	webServer server.Server,
	boxManager box.Manager,
	authManager auth.Manager,
	rtConfig config.Config,
) error

const (
	defaultNormalLogLevel = slog.LevelInfo
	defaultSimpleLogLevel = slog.LevelError
)

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

// Start the service.
func (kern *Kernel) Start(headline, lineServer bool, configFilename string) {
	for _, srvD := range kern.srvs {
		srvD.srv.Freeze()
	}
	if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) {
		kern.logLevelVar.Set(defaultSimpleLogLevel)
	}
	kern.wg.Add(1)
	signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM)
	go func() {
		// Wait for interrupt.
		sig := <-kern.interrupt
		if strSig := sig.String(); strSig != "" {
			kern.logger.Info("Shut down Zettelstore", "signal", strSig)
		}
		kern.doShutdown()
		kern.wg.Done()
	}()

	_ = kern.StartService(KernelService)
	if headline {
		logger := kern.logger
		logging.LogMandatory(logger, fmt.Sprintf(
			"%v %v (%v@%v/%v)",
			kern.core.GetCurConfig(CoreProgname),
			kern.core.GetCurConfig(CoreVersion),
			kern.core.GetCurConfig(CoreGoVersion),
			kern.core.GetCurConfig(CoreGoOS),
			kern.core.GetCurConfig(CoreGoArch),
		))
		logging.LogMandatory(logger, "Licensed under the latest version of the EUPL (European Union Public License)")
		if configFilename != "" {
			logging.LogMandatory(logger, "Configuration file found", "filename", configFilename)
		} else {
			logging.LogMandatory(logger, "No configuration file found / used")
		}
		if kern.core.GetCurConfig(CoreDebug).(bool) {
			logger.Info("----------------------------------------")
			logger.Info("DEBUG MODE, DO NO USE THIS IN PRODUCTION")
			logger.Info("----------------------------------------")
		}
		if kern.auth.GetCurConfig(AuthReadonly).(bool) {
			logger.Info("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)
		}
	}
}

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

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

// Shutdown the service. Waits for all concurrent activities to stop.
func (kern *Kernel) Shutdown(silent bool) {
	logging.LogTrace(kern.logger, "Shutdown")
	kern.interrupt <- &shutdownSignal{silent: silent}
}

type shutdownSignal struct{ silent bool }

func (s *shutdownSignal) String() string {
	if s.silent {
		return ""
	}
	return "shutdown"
}
func (*shutdownSignal) Signal() { /* Just a signal */ }

// --- Log operation ---------------------------------------------------------

// GetKernelLogger returns the kernel logger.
func (kern *Kernel) GetKernelLogger() *slog.Logger { return kern.logger }


// GetKernelLogLevel return the logging level of the kernel logger.
func (kern *Kernel) GetKernelLogLevel() slog.Level { return kern.logLevelVar.Level() }

// SetLogLevel sets the logging level for logger maintained by the kernel.
//
// Its syntax is: (SERVICE ":")? LEVEL (";" (SERVICE ":")? LEVEL)*.
func (kern *Kernel) SetLogLevel(logLevel string) {
	defaultLevel, srvLevel := kern.parseLogLevel(logLevel)

	kern.mx.RLock()
	defer kern.mx.RUnlock()

	for srvN, srvD := range kern.srvs {
		if lvl, found := srvLevel[srvN]; found {
			srvD.srv.SetLevel(lvl)
		} else if defaultLevel != logging.LevelMissing {
			srvD.srv.SetLevel(defaultLevel)
		}
	}
}

func (kern *Kernel) parseLogLevel(logLevel string) (slog.Level, map[Service]slog.Level) {
	defaultLevel := logging.LevelMissing
	srvLevel := map[Service]slog.Level{}
	for spec := range strings.SplitSeq(logLevel, ";") {
		vals := cleanLogSpec(strings.Split(spec, ":"))
		switch len(vals) {
		case 0:
		case 1:
			if lvl := logging.ParseLevel(vals[0]); lvl != logging.LevelMissing {
				defaultLevel = lvl
			}
		default:
			serviceText, levelText := vals[0], vals[1]
			if srv, found := kern.srvNames[serviceText]; found {
				if lvl := logging.ParseLevel(levelText); lvl != logging.LevelMissing {
					srvLevel[srv.srvnum] = lvl
				}
			}
		}
	}
	return defaultLevel, srvLevel
}
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
		}
	}
	return vals
}

// RetrieveLogEntries returns all buffered log entries.
func (kern *Kernel) RetrieveLogEntries() []LogEntry {
	return kern.logWriter.retrieveLogEntries()
}

// GetLastLogTime returns the time when the last logging with level > DEBUG happened.
func (kern *Kernel) GetLastLogTime() time.Time { return kern.logWriter.getLastLogTime() }

// LogRecover outputs some information about the previous panic.
func (kern *Kernel) LogRecover(name string, recoverInfo any) {
	stack := debug.Stack()
	kern.logger.Error().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name)
	kern.core.updateRecoverInfo(name, recoverInfo, stack)
}

// --- Profiling ---------------------------------------------------------

var errProfileInWork = errors.New("already profiling")
var errProfileNotFound = errors.New("profile not found")







|



|




|







412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
		}
	}
	return vals
}

// RetrieveLogEntries returns all buffered log entries.
func (kern *Kernel) RetrieveLogEntries() []LogEntry {
	return kern.dlogWriter.retrieveLogEntries()
}

// GetLastLogTime returns the time when the last logging with level > DEBUG happened.
func (kern *Kernel) GetLastLogTime() time.Time { return kern.dlogWriter.getLastLogTime() }

// LogRecover outputs some information about the previous panic.
func (kern *Kernel) LogRecover(name string, recoverInfo any) {
	stack := debug.Stack()
	kern.logger.Error(name, "recovered_from", recoverInfo, "stack", stack)
	kern.core.updateRecoverInfo(name, recoverInfo, stack)
}

// --- Profiling ---------------------------------------------------------

var errProfileInWork = errors.New("already profiling")
var errProfileNotFound = errors.New("profile not found")
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
		return errProfileInWork
	}
	if profileName == ProfileCPU {
		f, err := os.Create(fileName)
		if err != nil {
			return err
		}
		err = pprof.StartCPUProfile(f)
		if err != nil {
			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 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()







|
<
|




















|
<







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
		return errProfileInWork
	}
	if profileName == ProfileCPU {
		f, err := os.Create(fileName)
		if err != nil {
			return err
		}
		if err = pprof.StartCPUProfile(f); err != nil {

			_ = 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
	return profile.WriteTo(f, 0)

}

// 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()
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetStatistics()
	}
	return nil
}

// GetLogger returns a logger for the given service.
func (kern *Kernel) GetLogger(srvnum Service) *logger.Logger {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetLogger()
	}
	return kern.GetKernelLogger()
}







|







533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetStatistics()
	}
	return nil
}

// GetLogger returns a logger for the given service.
func (kern *Kernel) GetLogger(srvnum Service) *slog.Logger {
	kern.mx.RLock()
	defer kern.mx.RUnlock()
	if srvD, ok := kern.srvs[srvnum]; ok {
		return srvD.srv.GetLogger()
	}
	return kern.GetKernelLogger()
}
608
609
610
611
612
613
614
615
616
617
618






619
620
621
622
623
624
625
}

// dumpIndex writes some data about the internal index into a writer.
func (kern *Kernel) dumpIndex(w io.Writer) { kern.box.dumpIndex(w) }

type service interface {
	// Initialize the data for the service.
	Initialize(*logger.Logger)

	// Get service logger.
	GetLogger() *logger.Logger







	// ConfigDescriptions returns a sorted list of configuration descriptions.
	ConfigDescriptions() []serviceConfigDescription

	// SetConfig stores a configuration value.
	SetConfig(key, value string) error








|


|
>
>
>
>
>
>







619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
}

// dumpIndex writes some data about the internal index into a writer.
func (kern *Kernel) dumpIndex(w io.Writer) { kern.box.dumpIndex(w) }

type service interface {
	// Initialize the data for the service.
	Initialize(*slog.LevelVar, *slog.Logger)

	// Get service logger.
	GetLogger() *slog.Logger

	// Get the log level.
	GetLevel() slog.Level

	// Set the service log level.
	SetLevel(slog.Level)

	// ConfigDescriptions returns a sorted list of configuration descriptions.
	ConfigDescriptions() []serviceConfigDescription

	// SetConfig stores a configuration value.
	SetConfig(key, value string) error

669
670
671
672
673
674
675
676

677



678
679
680
681
682
683

684
685
686
687
688
689

// --- The kernel as a service -------------------------------------------

type kernelService struct {
	kernel *Kernel
}

func (*kernelService) Initialize(*logger.Logger)                      {}

func (ks *kernelService) GetLogger() *logger.Logger                   { return ks.kernel.logger }



func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil }
func (*kernelService) SetConfig(string, string) error                 { return errAlreadyFrozen }
func (*kernelService) GetCurConfig(string) any                        { return nil }
func (*kernelService) GetNextConfig(string) any                       { return nil }
func (*kernelService) GetCurConfigList(bool) []KeyDescrValue          { return nil }
func (*kernelService) GetNextConfigList() []KeyDescrValue             { return nil }

func (*kernelService) GetStatistics() []KeyValue                      { return nil }
func (*kernelService) Freeze()                                        {}
func (*kernelService) Start(*Kernel) error                            { return nil }
func (*kernelService) SwitchNextToCur()                               {}
func (*kernelService) IsStarted() bool                                { return true }
func (*kernelService) Stop(*Kernel)                                   {}







|
>
|
>
>
>






>
|
|
|
|
|
|
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711

// --- The kernel as a service -------------------------------------------

type kernelService struct {
	kernel *Kernel
}

func (*kernelService) Initialize(*slog.LevelVar, *slog.Logger) {}

func (ks *kernelService) GetLogger() *slog.Logger { return ks.kernel.logger }
func (ks *kernelService) GetLevel() slog.Level    { return ks.kernel.logLevelVar.Level() }
func (ks *kernelService) SetLevel(lvl slog.Level) { ks.kernel.logLevelVar.Set(lvl) }

func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil }
func (*kernelService) SetConfig(string, string) error                 { return errAlreadyFrozen }
func (*kernelService) GetCurConfig(string) any                        { return nil }
func (*kernelService) GetNextConfig(string) any                       { return nil }
func (*kernelService) GetCurConfigList(bool) []KeyDescrValue          { return nil }
func (*kernelService) GetNextConfigList() []KeyDescrValue             { return nil }

func (*kernelService) GetStatistics() []KeyValue { return nil }
func (*kernelService) Freeze()                   {}
func (*kernelService) Start(*Kernel) error       { return nil }
func (*kernelService) SwitchNextToCur()          {}
func (*kernelService) IsStarted() bool           { return true }
func (*kernelService) Stop(*Kernel)              {}
Changes to internal/kernel/log.go.
10
11
12
13
14
15
16


17

18
19
20
21
22
23
24






























































25
26

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

41
42
43
44
45
46
47

48
49
50

51

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

77
78
79
80
81
82
83
84
85
86
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package kernel

import (


	"os"

	"slices"
	"sync"
	"time"

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































































// kernelLogWriter adapts an io.Writer to a LogWriter
type kernelLogWriter struct {

	mx       sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries
	lastLog  time.Time
	buf      []byte
	writePos int
	data     []logEntry
	full     bool
}

// newKernelLogWriter creates a new LogWriter for kernel logging.
func newKernelLogWriter(capacity int) *kernelLogWriter {
	if capacity < 1 {
		capacity = 1
	}
	return &kernelLogWriter{

		lastLog: time.Now(),
		buf:     make([]byte, 0, 500),
		data:    make([]logEntry, capacity),
	}
}

func (klw *kernelLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error {

	klw.mx.Lock()

	if level > logger.DebugLevel {

		klw.lastLog = ts

		klw.data[klw.writePos] = logEntry{
			level:   level,
			ts:      ts,
			prefix:  prefix,
			msg:     msg,
			details: slices.Clone(details),
		}
		klw.writePos++
		if klw.writePos >= cap(klw.data) {
			klw.writePos = 0
			klw.full = true
		}
	}

	klw.buf = klw.buf[:0]
	buf := klw.buf
	addTimestamp(&buf, ts)
	buf = append(buf, ' ')
	buf = append(buf, level.Format()...)
	buf = append(buf, ' ')
	if prefix != "" {
		buf = append(buf, prefix...)
		buf = append(buf, ' ')
	}
	buf = append(buf, msg...)

	buf = append(buf, details...)
	buf = append(buf, '\n')
	_, err := os.Stdout.Write(buf)

	klw.mx.Unlock()
	return err
}

func addTimestamp(buf *[]byte, ts time.Time) {
	year, month, day := ts.Date()







>
>
|
>




|


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


>









|




>






|
>


|
>
|
>


















|






>


|







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

package kernel

import (
	"bytes"
	"context"
	"io"
	"log/slog"
	"slices"
	"sync"
	"time"

	"zettelstore.de/z/internal/logging"
)

type kernelLogHandler struct {
	klw    *kernelLogWriter
	level  slog.Leveler
	system string
	attrs  string
}

func newKernelLogHandler(klw *kernelLogWriter, level slog.Leveler) *kernelLogHandler {
	return &kernelLogHandler{
		klw:    klw,
		level:  level,
		system: "",
		attrs:  "",
	}
}

func (klh *kernelLogHandler) Enabled(_ context.Context, level slog.Level) bool {
	return level >= klh.level.Level()
}

func (klh *kernelLogHandler) Handle(_ context.Context, rec slog.Record) error {
	var buf bytes.Buffer
	buf.WriteString(klh.attrs)
	rec.Attrs(func(attr slog.Attr) bool {
		if !attr.Equal(slog.Attr{}) {
			buf.WriteByte(' ')
			buf.WriteString(attr.Key)
			buf.WriteByte('=')
			buf.WriteString(attr.Value.Resolve().String())
		}
		return true
	})
	return klh.klw.writeMessage(rec.Level, rec.Time, klh.system, rec.Message, buf.Bytes())
}

func (klh *kernelLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	h := newKernelLogHandler(klh.klw, klh.level)
	for _, attr := range attrs {
		if attr.Equal(slog.Attr{}) {
			continue
		}
		if attr.Key == "system" {
			system := attr.Value.String()
			if len(system) < 6 {
				system += "     "[:6-len(system)]
			}
			h.system = system
			continue
		}
		h.attrs += attr.String()
	}
	return h
}

func (klh *kernelLogHandler) WithGroup(name string) slog.Handler {
	if name == "" {
		return klh
	}
	return klh
	// panic("kernelLogHandler.WithGroup(name) not implemented")
}

// kernelLogWriter adapts an io.Writer to a LogWriter
type kernelLogWriter struct {
	w        io.Writer
	mx       sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries
	lastLog  time.Time
	buf      []byte
	writePos int
	data     []logEntry
	full     bool
}

// newKernelLogWriter creates a new LogWriter for kernel logging.
func newKernelLogWriter(w io.Writer, capacity int) *kernelLogWriter {
	if capacity < 1 {
		capacity = 1
	}
	return &kernelLogWriter{
		w:       w,
		lastLog: time.Now(),
		buf:     make([]byte, 0, 500),
		data:    make([]logEntry, capacity),
	}
}

func (klw *kernelLogWriter) writeMessage(level slog.Level, ts time.Time, prefix, msg string, details []byte) error {
	details = bytes.TrimSpace(details)
	klw.mx.Lock()

	if level > slog.LevelDebug {
		if !ts.IsZero() {
			klw.lastLog = ts
		}
		klw.data[klw.writePos] = logEntry{
			level:   level,
			ts:      ts,
			prefix:  prefix,
			msg:     msg,
			details: slices.Clone(details),
		}
		klw.writePos++
		if klw.writePos >= cap(klw.data) {
			klw.writePos = 0
			klw.full = true
		}
	}

	klw.buf = klw.buf[:0]
	buf := klw.buf
	addTimestamp(&buf, ts)
	buf = append(buf, ' ')
	buf = append(buf, logging.LevelStringPad(level)...)
	buf = append(buf, ' ')
	if prefix != "" {
		buf = append(buf, prefix...)
		buf = append(buf, ' ')
	}
	buf = append(buf, msg...)
	buf = append(buf, ' ')
	buf = append(buf, details...)
	buf = append(buf, '\n')
	_, err := klw.w.Write(buf)

	klw.mx.Unlock()
	return err
}

func addTimestamp(buf *[]byte, ts time.Time) {
	year, month, day := ts.Date()
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
		b[bp] = byte('0' + i - q*10)
		i = q
	}
	*buf = append(*buf, b[:wid]...)
}

type logEntry struct {
	level   logger.Level
	ts      time.Time
	prefix  string
	msg     string
	details []byte
}

func (klw *kernelLogWriter) retrieveLogEntries() []LogEntry {







|







177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
		b[bp] = byte('0' + i - q*10)
		i = q
	}
	*buf = append(*buf, b[:wid]...)
}

type logEntry struct {
	level   slog.Level
	ts      time.Time
	prefix  string
	msg     string
	details []byte
}

func (klw *kernelLogWriter) retrieveLogEntries() []LogEntry {
150
151
152
153
154
155
156
157

158
	return klw.lastLog
}

func copyE2E(result *LogEntry, origin *logEntry) {
	result.Level = origin.level
	result.TS = origin.ts
	result.Prefix = origin.prefix
	result.Message = origin.msg + string(origin.details)

}







|
>

221
222
223
224
225
226
227
228
229
230
	return klw.lastLog
}

func copyE2E(result *LogEntry, origin *logEntry) {
	result.Level = origin.level
	result.TS = origin.ts
	result.Prefix = origin.prefix
	result.Message = origin.msg
	result.Details = string(origin.details)
}
Added internal/kernel/log_test.go.




















































































































































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

package kernel

import (
	"fmt"
	"log/slog"
	"strings"
	"testing"
	"testing/slogtest"
)

func TestLogHand(t *testing.T) {
	t.SkipNow() // Handler does not implement Groups
	var sb strings.Builder
	logWriter := newKernelLogWriter(&sb, 1000)
	h := newKernelLogHandler(logWriter, slog.LevelInfo)
	results := func() []map[string]any {
		var ms []map[string]any
		for _, entry := range logWriter.retrieveLogEntries() {
			m := map[string]any{
				slog.LevelKey:   entry.Level,
				slog.MessageKey: entry.Message,
			}
			if ts := entry.TS; !ts.IsZero() {
				m[slog.TimeKey] = ts
			}
			details := entry.Details
			fmt.Printf("%q\n", details)
			for len(details) > 0 {
				pos := strings.Index(details, "=")
				if pos <= 0 {
					break
				}
				key := details[:pos]
				details = details[pos+1:]

				if details == "" || details[0] == '[' {
					break
				}

				pos = strings.Index(details, " ")
				var val string
				if pos <= 0 {
					val = details
					details = ""
				} else {
					val = details[:pos]
					details = details[pos+1:]
				}
				fmt.Printf("key %q, val %q\n", key, val)
				m[key] = val
			}
			ms = append(ms, m)
		}
		return ms
	}
	err := slogtest.TestHandler(h, results)
	if err != nil {
		t.Error(err)
	}
	t.Error(sb.String())
}
Changes to internal/kernel/server.go.
12
13
14
15
16
17
18


19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//-----------------------------------------------------------------------------

package kernel

import (
	"bufio"
	"net"


)

func startLineServer(kern *Kernel, listenAddr string) error {
	ln, err := net.Listen("tcp", listenAddr)
	if err != nil {
		kern.logger.Error().Err(err).Msg("Unable to start administration console")
		return err
	}
	kern.logger.Mandatory().Str("listen", listenAddr).Msg("Start administration console")
	go func() { lineServer(ln, kern) }()
	return nil
}

func lineServer(ln net.Listener, kern *Kernel) {
	// Something may panic. Ensure a running line service.
	defer func() {
		if ri := recover(); ri != nil {
			kern.LogRecover("Line", ri)
			go lineServer(ln, kern)
		}
	}()

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

func handleLineConnection(conn net.Conn, kern *Kernel) {
	// Something may panic. Ensure a running connection.
	defer func() {
		if ri := recover(); ri != nil {
			kern.LogRecover("LineConn", ri)
			go handleLineConnection(conn, kern)
		}
	}()

	kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console")
	cmds := cmdSession{}
	cmds.initialize(conn, kern)
	s := bufio.NewScanner(conn)
	for s.Scan() {
		line := s.Text()
		if !cmds.executeLine(line) {
			break
		}
	}
	conn.Close()
}







>
>





|


|

















|




|











|









|

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

package kernel

import (
	"bufio"
	"net"

	"zettelstore.de/z/internal/logging"
)

func startLineServer(kern *Kernel, listenAddr string) error {
	ln, err := net.Listen("tcp", listenAddr)
	if err != nil {
		kern.logger.Error("Unable to start administration console", "err", err)
		return err
	}
	logging.LogMandatory(kern.logger, "Start administration console", "listen", listenAddr)
	go func() { lineServer(ln, kern) }()
	return nil
}

func lineServer(ln net.Listener, kern *Kernel) {
	// Something may panic. Ensure a running line service.
	defer func() {
		if ri := recover(); ri != nil {
			kern.LogRecover("Line", ri)
			go lineServer(ln, kern)
		}
	}()

	for {
		conn, err := ln.Accept()
		if err != nil {
			// handle error
			kern.logger.Error("Unable to accept connection", "err", err)
			break
		}
		go handleLineConnection(conn, kern)
	}
	_ = 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)
			go handleLineConnection(conn, kern)
		}
	}()

	logging.LogMandatory(kern.logger, "Start session on administration console", "from", conn.RemoteAddr().String())
	cmds := cmdSession{}
	cmds.initialize(conn, kern)
	s := bufio.NewScanner(conn)
	for s.Scan() {
		line := s.Text()
		if !cmds.executeLine(line) {
			break
		}
	}
	_ = conn.Close()
}
Changes to internal/kernel/web.go.
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27


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

42
43
44
45
46
47
48
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package kernel

import (
	"errors"

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



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

type webService struct {
	srvConfig
	mxService   sync.RWMutex
	srvw        server.Server
	setupServer SetupWebServerFunc
}

var errURLPrefixSyntax = errors.New("must not be empty and must start with '//'")

func (ws *webService) Initialize(logger *logger.Logger) {

	ws.logger = logger
	ws.descr = descriptionMap{
		WebAssetDir: {
			"Asset file  directory",
			func(val string) (any, error) {
				val = filepath.Clean(val)
				finfo, err := os.Stat(val)







>










>
>
|












|
>







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

package kernel

import (
	"errors"
	"log/slog"
	"net"
	"net/netip"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

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

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

type webService struct {
	srvConfig
	mxService   sync.RWMutex
	srvw        server.Server
	setupServer SetupWebServerFunc
}

var errURLPrefixSyntax = errors.New("must not be empty and must start with '//'")

func (ws *webService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) {
	ws.logLevelVar = levelVar
	ws.logger = logger
	ws.descr = descriptionMap{
		WebAssetDir: {
			"Asset file  directory",
			func(val string) (any, error) {
				val = filepath.Clean(val)
				finfo, err := os.Stat(val)
74
75
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},


		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),
			true,
		},
		WebTokenLifetimeHTML: {
			"Token lifetime HTML",







>
>




>







78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
				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},
		WebSxMaxNesting:     {"Max nesting of Sx calls", parseInt, true},
		WebTokenLifetimeAPI: {
			"Token lifetime API",
			makeDurationParser(10*time.Minute, 0, 1*time.Hour),
			true,
		},
		WebTokenLifetimeHTML: {
			"Token lifetime HTML",
103
104
105
106
107
108
109


110
111

112
113
114
115
116
117
118
119
120
			true,
		},
	}
	ws.next = interfaceMap{
		WebAssetDir:          "",
		WebBaseURL:           "http://127.0.0.1:23123/",
		WebListenAddress:     "127.0.0.1:23123",


		WebMaxRequestSize:    int64(16 * 1024 * 1024),
		WebPersistentCookie:  false,

		WebSecureCookie:      true,
		WebProfiling:         false,
		WebTokenLifetimeAPI:  1 * time.Hour,
		WebTokenLifetimeHTML: 10 * time.Minute,
		WebURLPrefix:         "/",
	}
}

func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc {







>
>


>

|







110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
			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,
		WebProfiling:         false,
		WebSecureCookie:      true,
		WebSxMaxNesting:      32 * 1024,
		WebTokenLifetimeAPI:  1 * time.Hour,
		WebTokenLifetimeHTML: 10 * time.Minute,
		WebURLPrefix:         "/",
	}
}

func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc {
131
132
133
134
135
136
137
138


139
140
141
142


143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
		}
		return defDur, nil
	}
}

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)


	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) {
		ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg(
			"url-prefix is not a suffix of base-url")
		return errWrongBasePrefix
	}

	if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() {
		ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled")
	}

	sd := server.ConfigData{
		Log:              ws.logger,
		ListenAddr:       listenAddr,
		BaseURL:          baseURL,
		URLPrefix:        urlPrefix,
		MaxRequestSize:   maxRequestSize,
		Auth:             kern.auth.manager,


		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 {
		ws.logger.Error().Err(err).Msg("Unable to create")
		return err
	}
	if err = srvw.Run(); err != nil {
		ws.logger.Error().Err(err).Msg("Unable to start")
		return err
	}
	ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service")
	ws.mxService.Lock()
	ws.srvw = srvw
	ws.mxService.Unlock()

	if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) {
		listenAddr := ws.GetNextConfig(WebListenAddress).(string)
		if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
			ws.logger.Mandatory().Msg(strings.Repeat("--------------------", 3))
			ws.logger.Mandatory().Msg("Open your browser and enter the following URL:")
			ws.logger.Mandatory().Msg("    http://localhost" + listenAddr[idx:])
			ws.logger.Mandatory().Msg("")
			ws.logger.Mandatory().Msg("If this does not work, try:")
			ws.logger.Mandatory().Msg("    http://127.0.0.1" + listenAddr[idx:])
		}
	}

	return nil
}

func (ws *webService) IsStarted() bool {
	ws.mxService.RLock()
	defer ws.mxService.RUnlock()
	return ws.srvw != nil
}

func (ws *webService) Stop(*Kernel) {
	ws.logger.Info().Msg("Stop Service")
	ws.srvw.Stop()
	ws.mxService.Lock()
	ws.srvw = nil
	ws.mxService.Unlock()
}

func (*webService) GetStatistics() []KeyValue {
	return nil
}







|
>
>




>
>







<
|




|









>
>







|



|


|







|
|
|
|
|
|













|









141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163

164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
		}
		return defDur, nil
	}
}

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

func (ws *webService) GetLogger() *slog.Logger { return ws.logger }
func (ws *webService) GetLevel() slog.Level    { return ws.logLevelVar.Level() }
func (ws *webService) SetLevel(l slog.Level)   { ws.logLevelVar.Set(l) }

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

		ws.logger.Error("url-prefix is not a suffix of base-url", "base-url", baseURL, "url-prefix", urlPrefix)
		return errWrongBasePrefix
	}

	if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() {
		ws.logger.Info("service may be reached from outside, but authentication is not enabled", "listen", listenAddr)
	}

	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 {
		ws.logger.Error("Unable to create", "err", err)
		return err
	}
	if err = srvw.Run(); err != nil {
		ws.logger.Error("Unable to start", "err", err)
		return err
	}
	ws.logger.Info("Start Service", "listen", listenAddr, "base-url", baseURL)
	ws.mxService.Lock()
	ws.srvw = srvw
	ws.mxService.Unlock()

	if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) {
		listenAddr := ws.GetNextConfig(WebListenAddress).(string)
		if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
			logging.LogMandatory(ws.logger, strings.Repeat("--------------------", 3))
			logging.LogMandatory(ws.logger, "Open your browser and enter the following URL:")
			logging.LogMandatory(ws.logger, "    http://localhost"+listenAddr[idx:])
			logging.LogMandatory(ws.logger, "")
			logging.LogMandatory(ws.logger, "If this does not work, try:")
			logging.LogMandatory(ws.logger, "    http://127.0.0.1"+listenAddr[idx:])
		}
	}

	return nil
}

func (ws *webService) IsStarted() bool {
	ws.mxService.RLock()
	defer ws.mxService.RUnlock()
	return ws.srvw != nil
}

func (ws *webService) Stop(*Kernel) {
	ws.logger.Info("Stop Service")
	ws.srvw.Stop()
	ws.mxService.Lock()
	ws.srvw = nil
	ws.mxService.Unlock()
}

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

// Package logger implements a logging package for use in the Zettelstore.
package logger

import (
	"context"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"t73f.de/r/zsc/domain/meta"
)

// Level defines the possible log levels
type Level uint8

// Constants for Level
const (
	NoLevel        Level = iota // the absent log level
	TraceLevel                  // Log most internal activities
	DebugLevel                  // Log most data updates
	InfoLevel                   // Log normal activities
	ErrorLevel                  // Log (persistent) errors
	MandatoryLevel              // Log only mandatory events
	NeverLevel                  // Logging is disabled
)

var logLevel = [...]string{
	"     ",
	"TRACE",
	"DEBUG",
	"INFO ",
	"ERROR",
	">>>>>",
	"NEVER",
}

var strLevel = [...]string{
	"",
	"trace",
	"debug",
	"info",
	"error",
	"mandatory",
	"disabled",
}

// IsValid returns true, if the level is a valid level
func (l Level) IsValid() bool { return TraceLevel <= l && l <= NeverLevel }

func (l Level) String() string {
	if l.IsValid() {
		return strLevel[l]
	}
	return strconv.Itoa(int(l))
}

// Format returns a string representation suitable for logging.
func (l Level) Format() string {
	if l.IsValid() {
		return logLevel[l]
	}
	return strconv.Itoa(int(l))
}

// ParseLevel returns the recognized level.
func ParseLevel(text string) Level {
	for lv := TraceLevel; lv <= NeverLevel; lv++ {
		if len(text) > 2 && strings.HasPrefix(strLevel[lv], text) {
			return lv
		}
	}
	return NoLevel
}

// Logger represents an objects that emits logging messages.
type Logger struct {
	lw        LogWriter
	levelVal  uint32
	prefix    string
	context   []byte
	topParent *Logger
	uProvider UserProvider
}

// LogWriter writes log messages to their specified destinations.
type LogWriter interface {
	WriteMessage(level Level, ts time.Time, prefix, msg string, details []byte) error
}

// New creates a new logger for the given service.
//
// This function must only be called from a kernel implementation, not from
// code that tries to log something.
func New(lw LogWriter, prefix string) *Logger {
	if prefix != "" && len(prefix) < 6 {
		prefix = (prefix + "     ")[:6]
	}
	result := &Logger{
		lw:        lw,
		levelVal:  uint32(InfoLevel),
		prefix:    prefix,
		context:   nil,
		uProvider: nil,
	}
	result.topParent = result
	return result
}

func newFromMessage(msg *Message) *Logger {
	if msg == nil {
		return nil
	}
	logger := msg.logger
	context := make([]byte, 0, len(msg.buf))
	context = append(context, msg.buf...)
	return &Logger{
		lw:        nil,
		levelVal:  0,
		prefix:    logger.prefix,
		context:   context,
		topParent: logger.topParent,
		uProvider: nil,
	}
}

// SetLevel sets the level of the logger.
func (l *Logger) SetLevel(newLevel Level) *Logger {
	if l != nil {
		if l.topParent != l {
			panic("try to set level for child logger")
		}
		atomic.StoreUint32(&l.levelVal, uint32(newLevel))
	}
	return l
}

// Level returns the current level of the given logger
func (l *Logger) Level() Level {
	if l != nil {
		return Level(atomic.LoadUint32(&l.levelVal))
	}
	return NeverLevel
}

// Trace creates a tracing message.
func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) }

// Debug creates a debug message.
func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) }

// Info creates a message suitable for information data.
func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) }

// Error creates a message suitable for errors.
func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) }

// Mandatory creates a message that will always logged, except when logging
// is disabled.
func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) }

// Clone creates a message to clone the logger.
func (l *Logger) Clone() *Message {
	msg := newMessage(l, NeverLevel)
	if msg != nil {
		msg.level = NoLevel
	}
	return msg
}

// UserProvider allows to retrieve an user metadata from a context.
type UserProvider interface {
	GetUser(ctx context.Context) *meta.Meta
}

// WithUser creates a derivied logger that allows to retrieve and log user identifer.
func (l *Logger) WithUser(up UserProvider) *Logger {
	return &Logger{
		lw:        nil,
		levelVal:  0,
		prefix:    l.prefix,
		context:   l.context,
		topParent: l.topParent,
		uProvider: up,
	}
}

func (l *Logger) writeMessage(level Level, msg string, details []byte) error {
	return l.topParent.lw.WriteMessage(level, time.Now().Local(), l.prefix, msg, details)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































































































































































































































































































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

package logger_test

import (
	"fmt"
	"os"
	"testing"
	"time"

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

func TestParseLevel(t *testing.T) {
	testcases := []struct {
		text string
		exp  logger.Level
	}{
		{"tra", logger.TraceLevel},
		{"deb", logger.DebugLevel},
		{"info", logger.InfoLevel},
		{"err", logger.ErrorLevel},
		{"manda", logger.MandatoryLevel},
		{"dis", logger.NeverLevel},
		{"d", logger.Level(0)},
	}
	for i, tc := range testcases {
		got := logger.ParseLevel(tc.text)
		if got != tc.exp {
			t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got)
		}
	}
}

func BenchmarkDisabled(b *testing.B) {
	log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel)
	for b.Loop() {
		log.Info().Str("key", "val").Msg("Benchmark")
	}
}

type stderrLogWriter struct{}

func (*stderrLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error {
	fmt.Fprintf(os.Stderr, "%v %v %v %v %v\n", level.Format(), ts, prefix, msg, string(details))
	return nil
}

type testLogWriter struct{}

func (*testLogWriter) WriteMessage(logger.Level, time.Time, string, string, []byte) error {
	return nil
}

func BenchmarkStrMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "")
	for b.Loop() {
		log.Info().Str("key", "val").Msg("Benchmark")
	}
}

func BenchmarkMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "")
	for b.Loop() {
		log.Info().Msg("Benchmark")
	}
}

func BenchmarkCloneStrMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child()
	for b.Loop() {
		log.Info().Msg("123456789")
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































































































































Deleted internal/logger/message.go.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package logger

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

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

// Message presents a message to log.
type Message struct {
	logger *Logger
	level  Level
	buf    []byte
}

func newMessage(logger *Logger, level Level) *Message {
	if logger != nil {
		if logger.topParent.Level() <= level {
			m := messagePool.Get().(*Message)
			m.logger = logger
			m.level = level
			m.buf = append(m.buf[:0], logger.context...)
			return m
		}
	}
	return nil
}

func recycleMessage(m *Message) {
	messagePool.Put(m)
}

var messagePool = &sync.Pool{
	New: func() any {
		return &Message{
			buf: make([]byte, 0, 500),
		}
	},
}

// Enabled returns whether the message will log or not.
func (m *Message) Enabled() bool {
	return m != nil && m.level != NeverLevel
}

// Str adds a string value to the full message
func (m *Message) Str(text, val string) *Message {
	if m.Enabled() {
		buf := append(m.buf, ',', ' ')
		buf = append(buf, text...)
		buf = append(buf, '=')
		m.buf = append(buf, val...)
	}
	return m
}

// Bool adds a boolean value to the full message
func (m *Message) Bool(text string, val bool) *Message {
	if val {
		m.Str(text, "true")
	} else {
		m.Str(text, "false")
	}
	return m
}

// Bytes adds a byte slice value to the full message
func (m *Message) Bytes(text string, val []byte) *Message {
	if m.Enabled() {
		buf := append(m.buf, ',', ' ')
		buf = append(buf, text...)
		buf = append(buf, '=')
		m.buf = append(buf, val...)
	}
	return m
}

// Err adds an error value to the full message
func (m *Message) Err(err error) *Message {
	if err != nil {
		return m.Str("error", err.Error())
	}
	return m
}

// Int adds an integer to the full message
func (m *Message) Int(text string, i int64) *Message {
	return m.Str(text, strconv.FormatInt(i, 10))
}

// Uint adds an unsigned integer to the full message
func (m *Message) Uint(text string, u uint64) *Message {
	return m.Str(text, strconv.FormatUint(u, 10))
}

// User adds the user-id field of the given user to the message.
func (m *Message) User(ctx context.Context) *Message {
	if m.Enabled() {
		if up := m.logger.uProvider; up != nil {
			if user := up.GetUser(ctx); user != nil {
				m.buf = append(m.buf, ", user="...)
				if userID, found := user.Get(meta.KeyUserID); found {
					m.buf = append(m.buf, userID...)
				} else {
					m.buf = append(m.buf, user.Zid.Bytes()...)
				}
			}
		}
	}
	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 {
		return m
	}
	if from := r.Header.Get("X-Forwarded-For"); from != "" {
		return m.Str("ip", from)
	}
	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)
		recycleMessage(m)
	}
}

// Child creates a child logger with context of this message.
func (m *Message) Child() *Logger {
	return newFromMessage(m)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


























































































































































































































































































































Added internal/logging/logging.go.


















































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//-----------------------------------------------------------------------------
// Copyright (c) 2025-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2025-present Detlef Stern
//-----------------------------------------------------------------------------

// Package logging provides some definitions to adapt package slog to Zettelstore needs
package logging

import (
	"context"
	"log/slog"
	"strings"

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

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

// Some additional log levels.
const (
	LevelMissing   slog.Level = -9999
	LevelTrace     slog.Level = -8
	LevelMandatory slog.Level = 9999
)

// LevelString returns a string naming the level.
func LevelString(level slog.Level) string {
	switch level {
	case LevelTrace:
		return "TRACE"
	case LevelMandatory:
		return ">>>>>"
	case slog.LevelDebug:
		return "DEBUG"
	case slog.LevelInfo:
		return "INFO"
	case slog.LevelError:
		return "ERROR"
	default:
		return level.String()
	}
}

// LevelStringPad returns a string naming the level. The string is a least 5 bytes long.
func LevelStringPad(level slog.Level) string {
	s := LevelString(level)
	if len(s) < 5 {
		s = s + "     "[0:5-len(s)]
	}
	return s
}

// LogTrace writes a trace log message.
func LogTrace(logger *slog.Logger, msg string, args ...any) {
	logger.Log(context.Background(), LevelTrace, msg, args...)
}

// LogMandatory writes a mandatory log message.
func LogMandatory(logger *slog.Logger, msg string, args ...any) {
	logger.Log(context.Background(), LevelMandatory, msg, args...)
}

// ParseLevel returns the recognized level.
func ParseLevel(text string) slog.Level {
	switch strings.ToUpper(text) {
	case "TR", "TRA", "TRAC", "TRACE":
		return LevelTrace
	case "DE", "DEB", "DEBU", "DEBUG":
		return slog.LevelDebug
	case "IN", "INF", "INFO":
		return slog.LevelInfo
	case "WA", "WAR", "WARN":
		return slog.LevelWarn
	case "ER", "ERR", "ERRO", "ERROR":
		return slog.LevelError
	}
	return LevelMissing
}

// Err returns a log attribute, if an error occurred.
func Err(err error) slog.Attr {
	if err == nil {
		return slog.Attr{}
	}
	return slog.Any("err", err)
}

// User returns a log attribute indicating the currently user.
func User(ctx context.Context) slog.Attr {
	if um := user.GetCurrentUser(ctx); um != nil {
		if userID, found := um.Get(meta.KeyUserID); found {
			return slog.Any("user", userID)
		}
		return slog.String("user", um.Zid.String())
	}
	return slog.Attr{}
}
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
				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
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
159
160
161
162
163
164
165









































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









































Changes to internal/query/context.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
	if maxCost <= 0 {
		maxCost = 17
	}
	maxCount := spec.MaxCount
	if maxCount <= 0 {
		maxCount = 200
	}
	tasks := newQueue(startSeq, maxCost, maxCount, spec.MinCount, port)
	isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward
	isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward
	result := make([]*meta.Meta, 0, max(spec.MinCount, 16))
	for {
		m, cost, level := tasks.next()
		if m == nil {
			break
		}



		result = append(result, m)

		for key, val := range m.ComputedRest() {
			tasks.addPair(ctx, key, val, cost, level, isBackward, isForward)
		}
		if !spec.Full {
			continue
		}
		tasks.addTags(ctx, m.GetFields(meta.KeyTags), cost, level)
	}
	return result
}

type ztlCtxItem struct {
	cost  float64
	meta  *meta.Meta







|








>
>
>





|
|

<







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
	if maxCost <= 0 {
		maxCost = 17
	}
	maxCount := spec.MaxCount
	if maxCount <= 0 {
		maxCount = 200
	}
	tasks := newContextQueue(startSeq, maxCost, maxCount, spec.MinCount, port)
	isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward
	isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward
	result := make([]*meta.Meta, 0, max(spec.MinCount, 16))
	for {
		m, cost, level := tasks.next()
		if m == nil {
			break
		}
		if level == 1 {
			cost = min(cost, 4.0)
		}
		result = append(result, m)

		for key, val := range m.ComputedRest() {
			tasks.addPair(ctx, key, val, cost, level, isBackward, isForward)
		}
		if spec.Full {
			tasks.addTags(ctx, m.GetFields(meta.KeyTags), cost, level)
		}

	}
	return result
}

type ztlCtxItem struct {
	cost  float64
	meta  *meta.Meta
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
	maxCount int
	minCount int
	tagMetas map[string][]*meta.Meta
	tagZids  map[string]*idset.Set // just the zids of tagMetas
	metaZid  map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags
}

func newQueue(startSeq []*meta.Meta, maxCost float64, maxCount, minCount int, port ContextPort) *contextTask {
	result := &contextTask{
		port:     port,
		seen:     idset.New(),
		maxCost:  maxCost,
		maxCount: max(maxCount, minCount),
		minCount: minCount,
		tagMetas: make(map[string][]*meta.Meta),







|







163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
	maxCount int
	minCount int
	tagMetas map[string][]*meta.Meta
	tagZids  map[string]*idset.Set // just the zids of tagMetas
	metaZid  map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags
}

func newContextQueue(startSeq []*meta.Meta, maxCost float64, maxCount, minCount int, port ContextPort) *contextTask {
	result := &contextTask{
		port:     port,
		seen:     idset.New(),
		maxCost:  maxCost,
		maxCount: max(maxCount, minCount),
		minCount: minCount,
		tagMetas: make(map[string][]*meta.Meta),
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
		ct.addIDSet(ctx, newCost, level, value)
	}
}

func contextCost(key string) float64 {
	switch key {
	case meta.KeyFolge, meta.KeyPrecursor:
		return 0.1
	case meta.KeySequel, meta.KeyPrequel:
		return 1.0
	case meta.KeySuccessors, meta.KeyPredecessor:
		return 7
	}
	return 2
}







|







215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
		ct.addIDSet(ctx, newCost, level, value)
	}
}

func contextCost(key string) float64 {
	switch key {
	case meta.KeyFolge, meta.KeyPrecursor:
		return 0.2
	case meta.KeySequel, meta.KeyPrequel:
		return 1.0
	case meta.KeySuccessors, meta.KeyPredecessor:
		return 7
	}
	return 2
}
Changes to internal/query/parser.go.
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
		inp.SkipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
		}
	}

	hasContext := false
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {

			if hasContext {


				inp.SetPos(pos)


				break

			}








			q = ps.parseContext(q)
			hasContext = true
			continue
		}
		inp.SetPos(pos)
		if q == nil || len(q.zids) == 0 {
			break
		}
		if ps.acceptSingleKw(api.IdentDirective) {
			q.directives = append(q.directives, &IdentSpec{})







|






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








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
		inp.SkipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
		}
	}

	hasContext, hasThread := false, false
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if !hasContext && ps.acceptSingleKw(api.ContextDirective) {
			q = ps.parseContext(q)
			hasContext = true
			continue
		}
		inp.SetPos(pos)
		if !hasThread && ps.acceptSingleKw(api.FolgeDirective) {
			q = ps.parseThread(q, true, false)
			hasThread = true
			continue
		}
		inp.SetPos(pos)
		if !hasThread && ps.acceptSingleKw(api.SequelDirective) {
			q = ps.parseThread(q, false, true)
			hasThread = true
			continue
		}
		inp.SetPos(pos)
		if !hasThread && ps.acceptSingleKw(api.ThreadDirective) {
			q = ps.parseThread(q, true, true)
			hasThread = true
			continue
		}
		inp.SetPos(pos)
		if q == nil || len(q.zids) == 0 {
			break
		}
		if ps.acceptSingleKw(api.IdentDirective) {
			q.directives = append(q.directives, &IdentSpec{})
229
230
231
232
233
234
235


236

237
238
239
240
241
242
243
		if ps.acceptKwArgs(api.CostDirective) {
			if ps.parseCost(spec) {
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MaxDirective) {


			if ps.parseMaxCount(spec) {

				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MinDirective) {
			if ps.parseMinCount(spec) {
				continue







>
>
|
>







243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
		if ps.acceptKwArgs(api.CostDirective) {
			if ps.parseCost(spec) {
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MaxDirective) {
			if num, ok := ps.scanPosInt(); ok {
				if spec.MaxCount == 0 || spec.MaxCount >= num {
					spec.MaxCount = num
				}
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MinDirective) {
			if ps.parseMinCount(spec) {
				continue
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283








































284
285
286
287
288
289
290
		return false
	}
	if spec.MaxCost == 0 || spec.MaxCost >= num {
		spec.MaxCost = num
	}
	return true
}
func (ps *parserState) parseMaxCount(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
	}
	if spec.MaxCount == 0 || spec.MaxCount >= num {
		spec.MaxCount = num
	}
	return true
}
func (ps *parserState) parseMinCount(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
	}
	if spec.MinCount == 0 || spec.MinCount <= num {
		spec.MinCount = num
	}
	return true
}









































func (ps *parserState) parseUnlinked(q *Query) *Query {
	inp := ps.inp

	spec := &UnlinkedSpec{}
	for {
		inp.SkipSpace()







<
<
<
<
|
<
<
<
<
<










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







274
275
276
277
278
279
280




281





282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
		return false
	}
	if spec.MaxCost == 0 || spec.MaxCost >= num {
		spec.MaxCost = num
	}
	return true
}










func (ps *parserState) parseMinCount(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
	}
	if spec.MinCount == 0 || spec.MinCount <= num {
		spec.MinCount = num
	}
	return true
}

func (ps *parserState) parseThread(q *Query, isFolge, isSequel bool) *Query {
	inp := ps.inp
	spec := &ThreadSpec{IsFolge: isFolge, IsSequel: isSequel}
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.BackwardDirective) {
			spec.IsBackward = true
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.ForwardDirective) {
			spec.IsForward = true
			continue
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MaxDirective) {
			if num, ok := ps.scanPosInt(); ok {
				if spec.MaxCount == 0 || spec.MaxCount >= num {
					spec.MaxCount = num
				}
				continue
			}
		}
		inp.SetPos(pos)

		break
	}
	if !spec.IsForward && !spec.IsBackward {
		spec.IsForward = true
		spec.IsBackward = true
	}
	q = createIfNeeded(q)
	q.directives = append(q.directives, spec)
	return q
}

func (ps *parserState) parseUnlinked(q *Query) *Query {
	inp := ps.inp

	spec := &UnlinkedSpec{}
	for {
		inp.SkipSpace()
Changes to internal/query/parser_test.go.
50
51
52
53
54
55
56

















































57
58
59
60
61
62
63
		{"1 CONTEXT |  N", "00000000000001 CONTEXT | N"},
		{"1 1 CONTEXT", "00000000000001 CONTEXT"},
		{"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"},
		{"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"},
		{"1 CONTEXT|N", "00000000000001 CONTEXT | N"},

		{"CONTEXT 0", "CONTEXT 0"},


















































		{"1 UNLINKED", "00000000000001 UNLINKED"},
		{"UNLINKED", "UNLINKED"},
		{"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"},
		{"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"},

		{"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"},







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







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
		{"1 CONTEXT |  N", "00000000000001 CONTEXT | N"},
		{"1 1 CONTEXT", "00000000000001 CONTEXT"},
		{"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"},
		{"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"},
		{"1 CONTEXT|N", "00000000000001 CONTEXT | N"},

		{"CONTEXT 0", "CONTEXT 0"},

		{"FOLGE", "FOLGE"}, {"FOLGE a", "FOLGE a"},
		{"0 FOLGE", "0 FOLGE"}, {"1 FOLGE", "00000000000001 FOLGE"},
		{"1 FOLGE FOLGE", "00000000000001 FOLGE FOLGE"},
		{"00000000000001 FOLGE", "00000000000001 FOLGE"},
		{"100000000000001 FOLGE", "100000000000001 FOLGE"},
		{"1 FOLGE BACKWARD", "00000000000001 FOLGE BACKWARD"},
		{"1 FOLGE FORWARD", "00000000000001 FOLGE FORWARD"},
		{"1 FOLGE MAX 5", "00000000000001 FOLGE MAX 5"}, {"1 FOLGE MAX y", "00000000000001 FOLGE MAX y"},
		{"1 FOLGE |  N", "00000000000001 FOLGE | N"},
		{"1 1 FOLGE", "00000000000001 FOLGE"},
		{"1 2 FOLGE", "00000000000001 00000000000002 FOLGE"},
		{"2 1 FOLGE", "00000000000002 00000000000001 FOLGE"},
		{"1 FOLGE|N", "00000000000001 FOLGE | N"},

		{"FOLGE 0", "FOLGE 0"},

		{"SEQUEL", "SEQUEL"}, {"SEQUEL a", "SEQUEL a"},
		{"0 SEQUEL", "0 SEQUEL"}, {"1 SEQUEL", "00000000000001 SEQUEL"},
		{"1 SEQUEL SEQUEL", "00000000000001 SEQUEL SEQUEL"},
		{"00000000000001 SEQUEL", "00000000000001 SEQUEL"},
		{"100000000000001 SEQUEL", "100000000000001 SEQUEL"},
		{"1 SEQUEL BACKWARD", "00000000000001 SEQUEL BACKWARD"},
		{"1 SEQUEL FORWARD", "00000000000001 SEQUEL FORWARD"},
		{"1 SEQUEL MAX 5", "00000000000001 SEQUEL MAX 5"}, {"1 SEQUEL MAX y", "00000000000001 SEQUEL MAX y"},
		{"1 SEQUEL |  N", "00000000000001 SEQUEL | N"},
		{"1 1 SEQUEL", "00000000000001 SEQUEL"},
		{"1 2 SEQUEL", "00000000000001 00000000000002 SEQUEL"},
		{"2 1 SEQUEL", "00000000000002 00000000000001 SEQUEL"},
		{"1 SEQUEL|N", "00000000000001 SEQUEL | N"},

		{"SEQUEL 0", "SEQUEL 0"},

		{"THREAD", "THREAD"}, {"THREAD a", "THREAD a"},
		{"0 THREAD", "0 THREAD"}, {"1 THREAD", "00000000000001 THREAD"},
		{"1 THREAD THREAD", "00000000000001 THREAD THREAD"},
		{"00000000000001 THREAD", "00000000000001 THREAD"},
		{"100000000000001 THREAD", "100000000000001 THREAD"},
		{"1 THREAD FULL", "00000000000001 THREAD FULL"},
		{"1 THREAD BACKWARD", "00000000000001 THREAD BACKWARD"},
		{"1 THREAD FORWARD", "00000000000001 THREAD FORWARD"},
		{"1 THREAD MAX 5", "00000000000001 THREAD MAX 5"}, {"1 THREAD MAX y", "00000000000001 THREAD MAX y"},
		{"1 THREAD |  N", "00000000000001 THREAD | N"},
		{"1 1 THREAD", "00000000000001 THREAD"},
		{"1 2 THREAD", "00000000000001 00000000000002 THREAD"},
		{"2 1 THREAD", "00000000000002 00000000000001 THREAD"},
		{"1 THREAD|N", "00000000000001 THREAD | N"},

		{"THREAD 0", "THREAD 0"},

		{"1 UNLINKED", "00000000000001 UNLINKED"},
		{"UNLINKED", "UNLINKED"},
		{"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"},
		{"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"},

		{"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"},
Changes to 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
	space bool
}

var bsSpace = []byte{' '}

func (pe *PrintEnv) printSpace() {
	if pe.space {
		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) writeStrings(sSeq ...string) {
	for _, s := range sSeq {
		io.WriteString(pe.w, s)
	}
}

func (pe *PrintEnv) printZids(zids []id.Zid) {
	for i, zid := range zids {
		if i > 0 {
			pe.printSpace()







|




|
|


|







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)
		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) writeStrings(sSeq ...string) {
	for _, s := range sSeq {
		_, _ = io.WriteString(pe.w, s)
	}
}

func (pe *PrintEnv) printZids(zids []id.Zid) {
	for i, zid := range zids {
		if i > 0 {
			pe.printSpace()
Added internal/query/thread.go.








































































































































































































































































































































































































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

package query

import (
	"container/heap"
	"context"

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

// ThreadSpec contains all information for a thread directive.
type ThreadSpec struct {
	IsFolge    bool
	IsSequel   bool
	IsForward  bool
	IsBackward bool
	MaxCount   int
}

// Print the spec on the given print environment.
func (spec *ThreadSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	if spec.IsFolge {
		if spec.IsSequel {
			pe.writeString(api.ThreadDirective)
		} else {
			pe.writeString(api.FolgeDirective)
		}
	} else if spec.IsSequel {
		pe.writeString(api.SequelDirective)
	} else {
		panic("neither folge nor sequel")
	}

	if spec.IsForward {
		if !spec.IsBackward {
			pe.printSpace()
			pe.writeString(api.ForwardDirective)
		}
	} else if spec.IsBackward {
		pe.printSpace()
		pe.writeString(api.BackwardDirective)
	} else {
		panic("neither forward nor backward")
	}

	pe.printPosInt(api.MaxDirective, spec.MaxCount)
}

// ThreadPort is the collection of box methods needed by this directive.
type ThreadPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

// Execute the specification.
func (spec *ThreadSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ThreadPort) []*meta.Meta {
	tasks := newThreadQueue(startSeq, spec.MaxCount, port)
	result := make([]*meta.Meta, 0, 16)
	for {
		m, level := tasks.next()
		if m == nil {
			break
		}
		result = append(result, m)

		for key, val := range m.ComputedRest() {
			tasks.addPair(ctx, key, val, level, spec)
		}
	}
	return result
}

type ztlThreadItem struct {
	meta  *meta.Meta
	level uint
}
type ztlThreadQueue []ztlThreadItem

func (q ztlThreadQueue) Len() int { return len(q) }
func (q ztlThreadQueue) Less(i, j int) bool {
	if levelI, levelJ := q[i].level, q[j].level; levelI < levelJ {
		return true
	} else if levelI == levelJ {
		return q[i].meta.Zid < q[j].meta.Zid
	}
	return false
}
func (q ztlThreadQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }
func (q *ztlThreadQueue) Push(x any)   { *q = append(*q, x.(ztlThreadItem)) }
func (q *ztlThreadQueue) Pop() any {
	old := *q
	n := len(old)
	item := old[n-1]
	old[n-1].meta = nil // avoid memory leak
	*q = old[0 : n-1]
	return item
}

type threadTask struct {
	port     ThreadPort
	seen     *idset.Set
	queue    ztlThreadQueue
	maxCount int
}

func newThreadQueue(startSeq []*meta.Meta, maxCount int, port ThreadPort) *threadTask {
	result := &threadTask{
		port:     port,
		seen:     idset.New(),
		maxCount: maxCount,
	}

	queue := make(ztlThreadQueue, 0, len(startSeq))
	for _, m := range startSeq {
		queue = append(queue, ztlThreadItem{meta: m})
	}
	heap.Init(&queue)
	result.queue = queue
	return result
}

func (ct *threadTask) next() (*meta.Meta, uint) {
	for len(ct.queue) > 0 {
		item := heap.Pop(&ct.queue).(ztlThreadItem)
		m := item.meta
		zid := m.Zid
		if ct.seen.Contains(zid) {
			continue
		}
		level := item.level
		if ct.hasEnough(level) {
			break
		}
		ct.seen.Add(zid)
		return m, item.level
	}
	return nil, 0
}

func (ct *threadTask) hasEnough(level uint) bool {
	maxCount := ct.maxCount
	if level <= 1 || ct.maxCount <= 0 {
		// Always add direct descendants of the initial zettel
		return false
	}
	return maxCount <= ct.seen.Length()
}

func (ct *threadTask) addPair(ctx context.Context, key string, value meta.Value, level uint, spec *ThreadSpec) {
	isFolge, isSequel, isBackward, isForward := spec.IsFolge, spec.IsSequel, spec.IsBackward, spec.IsForward
	switch key {
	case meta.KeyPrecursor:
		if !isFolge || !isBackward {
			return
		}
	case meta.KeyFolge:
		if !isFolge || !isForward {
			return
		}
	case meta.KeyPrequel:
		if !isSequel || !isBackward {
			return
		}
	case meta.KeySequel:
		if !isSequel || !isForward {
			return
		}
	default:
		return
	}
	elems := value.AsSlice()
	for _, val := range elems {
		if zid, errParse := id.Parse(val); errParse == nil {
			if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil {
				if !ct.seen.Contains(m.Zid) {
					heap.Push(&ct.queue, ztlThreadItem{meta: m, level: level + 1})
				}
			}
		}
	}
}
Changes to internal/usecase/authenticate.go.
11
12
13
14
15
16
17

18
19
20
21

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

package usecase

import (
	"context"

	"math/rand/v2"
	"net/http"
	"time"


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

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

// Authenticate is the data for this use case.
type Authenticate struct {
	log       *logger.Logger
	token     auth.TokenManager
	ucGetUser *GetUser
}

// NewAuthenticate creates a new use case.
func NewAuthenticate(log *logger.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate {
	return Authenticate{
		log:       log,
		token:     token,
		ucGetUser: ucGetUser,
	}
}

// Run executes the use case.
//
// 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")
		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")
			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")
		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(
		"$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) {
	jitter := time.Duration(rand.IntN(100)-50) * time.Millisecond
	if elapsed := time.Since(start); elapsed+minDelay < durDelay {
		time.Sleep(durDelay - elapsed + jitter)
	} else {
		time.Sleep(minDelay + jitter)
	}
}

// IsAuthenticatedPort contains method for this usecase.
type IsAuthenticatedPort interface {
	GetUser(context.Context) *meta.Meta
}

// IsAuthenticated cheks if the caller is alrwady authenticated.
type IsAuthenticated struct {
	log   *logger.Logger
	port  IsAuthenticatedPort
	authz auth.AuthzManager
}

// NewIsAuthenticated creates a new use case object.
func NewIsAuthenticated(log *logger.Logger, port IsAuthenticatedPort, authz auth.AuthzManager) IsAuthenticated {
	return IsAuthenticated{
		log:   log,
		port:  port,
		authz: authz,
	}
}

// IsAuthenticatedResult is an enumeration.
type IsAuthenticatedResult uint8

// Values for IsAuthenticatedResult.
const (
	_ IsAuthenticatedResult = iota
	IsAuthenticatedDisabled
	IsAuthenticatedAndValid
	IsAuthenticatedAndInvalid
)

// Run executes the use case.
func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult {
	if !uc.authz.WithAuth() {
		uc.log.Info().Str("auth", "disabled").Msg("IsAuthenticated")
		return IsAuthenticatedDisabled
	}
	if uc.port.GetUser(ctx) == nil {
		uc.log.Info().Msg("IsAuthenticated is false")
		return IsAuthenticatedAndInvalid
	}
	uc.log.Info().Msg("IsAuthenticated is true")
	return IsAuthenticatedAndValid
}







>




>





<




|





|
















|







|





|


|


|


|






|

















|


|

|
|
|



|

|
|
|

















|


|
|


|


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

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"context"
	"log/slog"
	"math/rand/v2"
	"net/http"
	"time"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/auth/cred"

)

// Authenticate is the data for this use case.
type Authenticate struct {
	log       *slog.Logger
	token     auth.TokenManager
	ucGetUser *GetUser
}

// NewAuthenticate creates a new use case.
func NewAuthenticate(log *slog.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate {
	return Authenticate{
		log:       log,
		token:     token,
		ucGetUser: ucGetUser,
	}
}

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

// compensateCompare if normal comapare is not possible, to avoid timing hints.
func compensateCompare() {
	_, _ = 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) {
	jitter := time.Duration(rand.IntN(100)-50) * time.Millisecond
	if elapsed := time.Since(start); elapsed+minDelay < durDelay {
		time.Sleep(durDelay - elapsed + jitter)
	} else {
		time.Sleep(minDelay + jitter)
	}
}

// IsAuthenticatedPort contains method for this usecase.
type IsAuthenticatedPort interface {
	GetCurrentUser(context.Context) *meta.Meta
}

// IsAuthenticated cheks if the caller is already authenticated.
type IsAuthenticated struct {
	logger *slog.Logger
	port   IsAuthenticatedPort
	authz  auth.AuthzManager
}

// NewIsAuthenticated creates a new use case object.
func NewIsAuthenticated(logger *slog.Logger, port IsAuthenticatedPort, authz auth.AuthzManager) IsAuthenticated {
	return IsAuthenticated{
		logger: logger,
		port:   port,
		authz:  authz,
	}
}

// IsAuthenticatedResult is an enumeration.
type IsAuthenticatedResult uint8

// Values for IsAuthenticatedResult.
const (
	_ IsAuthenticatedResult = iota
	IsAuthenticatedDisabled
	IsAuthenticatedAndValid
	IsAuthenticatedAndInvalid
)

// Run executes the use case.
func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult {
	if !uc.authz.WithAuth() {
		uc.logger.Info("IsAuthenticated", "auth", "disabled")
		return IsAuthenticatedDisabled
	}
	if uc.port.GetCurrentUser(ctx) == nil {
		uc.logger.Info("IsAuthenticated is false")
		return IsAuthenticatedAndInvalid
	}
	uc.logger.Info("IsAuthenticated is true")
	return IsAuthenticatedAndValid
}
Changes to internal/usecase/create_zettel.go.
11
12
13
14
15
16
17

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

package usecase

import (
	"context"

	"time"

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

	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/zettel"
)

// CreateZettelPort is the interface used by this use case.
type CreateZettelPort interface {
	// CreateZettel creates a new zettel.
	CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error)
}

// CreateZettel is the data for this use case.
type CreateZettel struct {
	log      *logger.Logger
	rtConfig config.Config
	port     CreateZettelPort
}

// NewCreateZettel creates a new use case.
func NewCreateZettel(log *logger.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel {
	return CreateZettel{
		log:      log,
		rtConfig: rtConfig,
		port:     port,
	}
}

// PrepareCopy the zettel for further modification.
func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel {







>






|











|





|

|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"context"
	"log/slog"
	"time"

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

	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/zettel"
)

// CreateZettelPort is the interface used by this use case.
type CreateZettelPort interface {
	// CreateZettel creates a new zettel.
	CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error)
}

// CreateZettel is the data for this use case.
type CreateZettel struct {
	logger   *slog.Logger
	rtConfig config.Config
	port     CreateZettelPort
}

// NewCreateZettel creates a new use case.
func NewCreateZettel(logger *slog.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel {
	return CreateZettel{
		logger:   logger,
		rtConfig: rtConfig,
		port:     port,
	}
}

// PrepareCopy the zettel for further modification.
func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel {
149
150
151
152
153
154
155
156
157
158

	m.Set(meta.KeyCreated, meta.Value(time.Now().Local().Format(id.TimestampLayout)))
	m.Delete(meta.KeyModified)
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content.TrimSpace()
	zid, err := uc.port.CreateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel")
	return zid, err
}







|


150
151
152
153
154
155
156
157
158
159

	m.Set(meta.KeyCreated, meta.Value(time.Now().Local().Format(id.TimestampLayout)))
	m.Delete(meta.KeyModified)
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content.TrimSpace()
	zid, err := uc.port.CreateZettel(ctx, zettel)
	uc.logger.Info("Create zettel", "zid", zid, logging.User(ctx), logging.Err(err))
	return zid, err
}
Changes to internal/usecase/delete_zettel.go.
11
12
13
14
15
16
17

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

package usecase

import (
	"context"


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

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

// DeleteZettelPort is the interface used by this use case.
type DeleteZettelPort interface {
	// DeleteZettel removes the zettel from the box.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

// DeleteZettel is the data for this use case.
type DeleteZettel struct {
	log  *logger.Logger
	port DeleteZettelPort
}

// NewDeleteZettel creates a new use case.
func NewDeleteZettel(log *logger.Logger, port DeleteZettelPort) DeleteZettel {
	return DeleteZettel{log: log, port: port}
}

// Run executes the use case.
func (uc *DeleteZettel) Run(ctx context.Context, zid id.Zid) error {
	err := uc.port.DeleteZettel(ctx, zid)
	uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Delete zettel")
	return err
}







>



|










|
|



|
|





|


11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"context"
	"log/slog"

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

	"zettelstore.de/z/internal/logging"
)

// DeleteZettelPort is the interface used by this use case.
type DeleteZettelPort interface {
	// DeleteZettel removes the zettel from the box.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

// DeleteZettel is the data for this use case.
type DeleteZettel struct {
	logger *slog.Logger
	port   DeleteZettelPort
}

// NewDeleteZettel creates a new use case.
func NewDeleteZettel(logger *slog.Logger, port DeleteZettelPort) DeleteZettel {
	return DeleteZettel{logger: logger, port: port}
}

// Run executes the use case.
func (uc *DeleteZettel) Run(ctx context.Context, zid id.Zid) error {
	err := uc.port.DeleteZettel(ctx, zid)
	uc.logger.Info("Delete zettel", "zid", zid, logging.User(ctx), logging.Err(err))
	return err
}
Changes to internal/usecase/get_all_zettel.go.
13
14
15
16
17
18
19

20
21
22
23
24
25
26

package usecase

import (
	"context"

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

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

// GetAllZettelPort is the interface used by this use case.
type GetAllZettelPort interface {
	GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error)
}







>







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

package usecase

import (
	"context"

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

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

// GetAllZettelPort is the interface used by this use case.
type GetAllZettelPort interface {
	GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error)
}
Changes to internal/usecase/get_references.go.
14
15
16
17
18
19
20

21
22
23
24
25
26
27
package usecase

import (
	"iter"

	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/collect"
)

// GetReferences is the usecase to retrieve references that occur in a zettel.
type GetReferences struct{}








>







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

import (
	"iter"

	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/collect"
)

// GetReferences is the usecase to retrieve references that occur in a zettel.
type GetReferences struct{}

Changes to internal/usecase/get_zettel.go.
13
14
15
16
17
18
19

20
21
22
23
24
25
26

package usecase

import (
	"context"

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

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

// GetZettelPort is the interface used by this use case.
type GetZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)







>







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

package usecase

import (
	"context"

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

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

// GetZettelPort is the interface used by this use case.
type GetZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
Changes to internal/usecase/query.go.
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
	for _, dir := range directives {
		if len(metaSeq) == 0 {
			return nil
		}
		switch ds := dir.(type) {
		case *query.ContextSpec:
			metaSeq = uc.processContextDirective(ctx, ds, metaSeq)


		case *query.IdentSpec:
			// Nothing to do.
		case *query.ItemsSpec:
			metaSeq = uc.processItemsDirective(ctx, ds, metaSeq)
		case *query.UnlinkedSpec:
			metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq)
		default:
			panic(fmt.Sprintf("Unknown directive %T", ds))
		}
	}
	if len(metaSeq) == 0 {
		return nil
	}
	return metaSeq
}

func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta {
	return spec.Execute(ctx, metaSeq, uc.port)
}





func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		if err != nil {
			continue







>
>















>



>
>
>
>







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
	for _, dir := range directives {
		if len(metaSeq) == 0 {
			return nil
		}
		switch ds := dir.(type) {
		case *query.ContextSpec:
			metaSeq = uc.processContextDirective(ctx, ds, metaSeq)
		case *query.ThreadSpec:
			metaSeq = uc.processThreadDirective(ctx, ds, metaSeq)
		case *query.IdentSpec:
			// Nothing to do.
		case *query.ItemsSpec:
			metaSeq = uc.processItemsDirective(ctx, ds, metaSeq)
		case *query.UnlinkedSpec:
			metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq)
		default:
			panic(fmt.Sprintf("Unknown directive %T", ds))
		}
	}
	if len(metaSeq) == 0 {
		return nil
	}
	return metaSeq
}

func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta {
	return spec.Execute(ctx, metaSeq, uc.port)
}

func (uc *Query) processThreadDirective(ctx context.Context, spec *query.ThreadSpec, metaSeq []*meta.Meta) []*meta.Meta {
	return spec.Execute(ctx, metaSeq, uc.port)
}

func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		if err != nil {
			continue
Changes to internal/usecase/refresh.go.
11
12
13
14
15
16
17

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

package usecase

import (
	"context"


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

// RefreshPort is the interface used by this use case.
type RefreshPort interface {
	Refresh(context.Context) error
}

// Refresh is the data for this use case.
type Refresh struct {
	log  *logger.Logger
	port RefreshPort
}

// NewRefresh creates a new use case.
func NewRefresh(log *logger.Logger, port RefreshPort) Refresh {
	return Refresh{log: log, port: port}
}

// Run executes the use case.
func (uc *Refresh) Run(ctx context.Context) error {
	err := uc.port.Refresh(ctx)
	uc.log.Info().User(ctx).Err(err).Msg("Refresh internal data")
	return err
}







>

|









|
|



|
|





|


11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"context"
	"log/slog"

	"zettelstore.de/z/internal/logging"
)

// RefreshPort is the interface used by this use case.
type RefreshPort interface {
	Refresh(context.Context) error
}

// Refresh is the data for this use case.
type Refresh struct {
	logger *slog.Logger
	port   RefreshPort
}

// NewRefresh creates a new use case.
func NewRefresh(logger *slog.Logger, port RefreshPort) Refresh {
	return Refresh{logger: logger, port: port}
}

// Run executes the use case.
func (uc *Refresh) Run(ctx context.Context) error {
	err := uc.port.Refresh(ctx)
	uc.logger.Info("Refresh internal data", logging.User(ctx), logging.Err(err))
	return err
}
Changes to internal/usecase/reindex.go.
11
12
13
14
15
16
17

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

package usecase

import (
	"context"


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

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

// ReIndexPort is the interface used by this use case.
type ReIndexPort interface {
	ReIndex(context.Context, id.Zid) error
}

// ReIndex is the data for this use case.
type ReIndex struct {
	log  *logger.Logger
	port ReIndexPort
}

// NewReIndex creates a new use case.
func NewReIndex(log *logger.Logger, port ReIndexPort) ReIndex {
	return ReIndex{log: log, port: port}
}

// Run executes the use case.
func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error {
	err := uc.port.ReIndex(ctx, zid)
	uc.log.Info().User(ctx).Err(err).Zid(zid).Msg("ReIndex zettel")
	return err
}







>



|









|
|



|
|





|


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
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"context"
	"log/slog"

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

	"zettelstore.de/z/internal/logging"
)

// ReIndexPort is the interface used by this use case.
type ReIndexPort interface {
	ReIndex(context.Context, id.Zid) error
}

// ReIndex is the data for this use case.
type ReIndex struct {
	logger *slog.Logger
	port   ReIndexPort
}

// NewReIndex creates a new use case.
func NewReIndex(logger *slog.Logger, port ReIndexPort) ReIndex {
	return ReIndex{logger: logger, port: port}
}

// Run executes the use case.
func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error {
	err := uc.port.ReIndex(ctx, zid)
	uc.logger.Info("ReIndex zettel", "zid", zid, logging.User(ctx), logging.Err(err))
	return err
}
Changes to internal/usecase/update_zettel.go.
11
12
13
14
15
16
17

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

package usecase

import (
	"context"


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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/zettel"
)

// UpdateZettelPort is the interface used by this use case.
type UpdateZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)

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

// UpdateZettel is the data for this use case.
type UpdateZettel struct {
	log  *logger.Logger
	port UpdateZettelPort
}

// NewUpdateZettel creates a new use case.
func NewUpdateZettel(log *logger.Logger, port UpdateZettelPort) UpdateZettel {
	return UpdateZettel{log: log, port: port}
}

// Run executes the use case.
func (uc *UpdateZettel) Run(ctx context.Context, zettel zettel.Zettel, hasContent bool) error {
	m := zettel.Meta
	oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid)
	if err != nil {







>





|














|
|



|
|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"context"
	"log/slog"

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

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/zettel"
)

// UpdateZettelPort is the interface used by this use case.
type UpdateZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)

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

// UpdateZettel is the data for this use case.
type UpdateZettel struct {
	logger *slog.Logger
	port   UpdateZettelPort
}

// NewUpdateZettel creates a new use case.
func NewUpdateZettel(logger *slog.Logger, port UpdateZettelPort) UpdateZettel {
	return UpdateZettel{logger: logger, port: port}
}

// Run executes the use case.
func (uc *UpdateZettel) Run(ctx context.Context, zettel zettel.Zettel, hasContent bool) error {
	m := zettel.Meta
	oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid)
	if err != nil {
69
70
71
72
73
74
75
76
77
78
	}

	if !hasContent {
		zettel.Content = oldZettel.Content
	}
	zettel.Content.TrimSpace()
	err = uc.port.UpdateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel")
	return err
}







|


70
71
72
73
74
75
76
77
78
79
	}

	if !hasContent {
		zettel.Content = oldZettel.Content
	}
	zettel.Content.TrimSpace()
	err = uc.port.UpdateZettel(ctx, zettel)
	uc.logger.Info("Update zettel", "zid", m.Zid, logging.User(ctx), logging.Err(err))
	return err
}
Changes to internal/web/adapter/api/api.go.
13
14
15
16
17
18
19

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

// Package api provides api handlers for web requests.
package api

import (
	"bytes"
	"context"

	"net/http"
	"time"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/web/server"
)

// API holds all data and methods for delivering API call results.
type API struct {
	log      *logger.Logger
	b        server.Builder
	authz    auth.AuthzManager
	token    auth.TokenManager
	rtConfig config.Config
	policy   auth.Policy

	tokenLifetime time.Duration
}

// New creates a new API object.
func New(log *logger.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager,
	rtConfig config.Config, pol auth.Policy) *API {
	a := &API{
		log:      log,
		b:        b,
		authz:    authz,
		token:    token,
		rtConfig: rtConfig,
		policy:   pol,

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration),
	}
	return a
}

// NewURLBuilder creates a new URL builder object with the given key.
func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) }

func (a *API) getAuthData(ctx context.Context) *server.AuthData {
	return server.GetAuthData(ctx)
}
func (a *API) withAuth() bool { return a.authz.WithAuth() }
func (a *API) getToken(ident *meta.Meta) ([]byte, error) {
	return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI)
}

func (a *API) reportUsecaseError(w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		a.log.Error().Err(err).Msg(text)
		http.Error(w, http.StatusText(code), code)
		return
	}
	// TODO: must call PrepareHeader somehow
	http.Error(w, text, code)
}

func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error {
	return adapter.WriteData(w, buf.Bytes(), contentType)
}

func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) {
	pol := a.policy
	user := server.GetUser(ctx)
	if pol.CanCreate(user, m) {
		result |= api.ZettelCanCreate
	}
	if pol.CanRead(user, m) {
		result |= api.ZettelCanRead
	}
	if pol.CanWrite(user, m, m) {







>







|
|
|






|










|


|














|
|









|













|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

// Package api provides api handlers for web requests.
package api

import (
	"bytes"
	"context"
	"log/slog"
	"net/http"
	"time"

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

	"zettelstore.de/z/internal/auth"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/web/server"
)

// API holds all data and methods for delivering API call results.
type API struct {
	logger   *slog.Logger
	b        server.Builder
	authz    auth.AuthzManager
	token    auth.TokenManager
	rtConfig config.Config
	policy   auth.Policy

	tokenLifetime time.Duration
}

// New creates a new API object.
func New(logger *slog.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager,
	rtConfig config.Config, pol auth.Policy) *API {
	a := &API{
		logger:   logger,
		b:        b,
		authz:    authz,
		token:    token,
		rtConfig: rtConfig,
		policy:   pol,

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration),
	}
	return a
}

// NewURLBuilder creates a new URL builder object with the given key.
func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) }

func (a *API) getAuthData(ctx context.Context) *user.AuthData {
	return user.GetAuthData(ctx)
}
func (a *API) withAuth() bool { return a.authz.WithAuth() }
func (a *API) getToken(ident *meta.Meta) ([]byte, error) {
	return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI)
}

func (a *API) reportUsecaseError(w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		a.logger.Error(text, "err", err)
		http.Error(w, http.StatusText(code), code)
		return
	}
	// TODO: must call PrepareHeader somehow
	http.Error(w, text, code)
}

func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error {
	return adapter.WriteData(w, buf.Bytes(), contentType)
}

func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) {
	pol := a.policy
	user := user.GetCurrentUser(ctx)
	if pol.CanCreate(user, m) {
		result |= api.ZettelCanCreate
	}
	if pol.CanRead(user, m) {
		result |= api.ZettelCanRead
	}
	if pol.CanWrite(user, m, m) {
Changes to internal/web/adapter/api/create_zettel.go.
69
70
71
72
73
74
75
76
77
78
79
			panic(encStr)
		}

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		if _, err = w.Write(result); err != nil {
			a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel")
		}
	})
}







|



69
70
71
72
73
74
75
76
77
78
79
			panic(encStr)
		}

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		if _, err = w.Write(result); err != nil {
			a.logger.Error("Create zettel", "err", err, "zid", newZid)
		}
	})
}
Changes to internal/web/adapter/api/get_data.go.
30
31
32
33
34
35
36
37
38
39
40
			sx.Int64(version.Major),
			sx.Int64(version.Minor),
			sx.Int64(version.Patch),
			sx.MakeString(version.Info),
			sx.MakeString(version.Hash),
		))
		if err != nil {
			a.log.Error().Err(err).Msg("Write Version Info")
		}
	})
}







|



30
31
32
33
34
35
36
37
38
39
40
			sx.Int64(version.Major),
			sx.Int64(version.Minor),
			sx.Int64(version.Patch),
			sx.MakeString(version.Info),
			sx.MakeString(version.Hash),
		))
		if err != nil {
			a.logger.Error("Write version info", "err", err)
		}
	})
}
Changes to internal/web/adapter/api/get_references.go.
18
19
20
21
22
23
24

25
26
27
28
29
30
31
	"iter"
	"net/http"

	"t73f.de/r/sx"
	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/content"
)

// MakeGetReferencesHandler creates a new HTTP handler to return various lists
// of zettel references.







>







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	"iter"
	"net/http"

	"t73f.de/r/sx"
	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/content"
)

// MakeGetReferencesHandler creates a new HTTP handler to return various lists
// of zettel references.
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
		}

		enc, _ := getEncoding(r, q)
		if enc == api.EncoderData {
			var lb sx.ListBuilder
			lb.Collect(zeroiter.MapSeq(seq, func(s string) sx.Object { return sx.MakeString(s) }))
			if err = a.writeObject(w, zid, lb.List()); err != nil {
				a.log.Error().Err(err).Zid(zid).Msg("write sx data")
			}
			return
		}

		var buf bytes.Buffer
		for s := range seq {
			buf.WriteString(s)
			buf.WriteByte('\n')
		}
		if err = writeBuffer(w, &buf, content.PlainText); err != nil {
			a.log.Error().Err(err).Zid(zid).Msg("Write Plain data")
		}
	})
}

func getExternalURLs(zn *ast.ZettelNode, ucGetReferences usecase.GetReferences) iter.Seq[string] {
	return zeroiter.MapSeq(
		ucGetReferences.RunByExternal(zn),
		func(ref *ast.Reference) string { return ref.Value },
	)
}







|










|










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
		}

		enc, _ := getEncoding(r, q)
		if enc == api.EncoderData {
			var lb sx.ListBuilder
			lb.Collect(zeroiter.MapSeq(seq, func(s string) sx.Object { return sx.MakeString(s) }))
			if err = a.writeObject(w, zid, lb.List()); err != nil {
				a.logger.Error("write sx data", "err", err, "zid", zid)
			}
			return
		}

		var buf bytes.Buffer
		for s := range seq {
			buf.WriteString(s)
			buf.WriteByte('\n')
		}
		if err = writeBuffer(w, &buf, content.PlainText); err != nil {
			a.logger.Error("write plain data", "err", err, "zid", zid)
		}
	})
}

func getExternalURLs(zn *ast.ZettelNode, ucGetReferences usecase.GetReferences) iter.Seq[string] {
	return zeroiter.MapSeq(
		ucGetReferences.RunByExternal(zn),
		func(ref *ast.Reference) string { return ref.Value },
	)
}
Changes to internal/web/adapter/api/get_zettel.go.
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

	case partContent:
		contentType = content.MIMEFromSyntax(string(z.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		_, err = z.Content.Write(&buf)
	}

	if err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if err = writeBuffer(w, &buf, contentType); err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("Write Plain data")
	}
}

func (a *API) writeSzData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)







|




|







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

	case partContent:
		contentType = content.MIMEFromSyntax(string(z.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		_, err = z.Content.Write(&buf)
	}

	if err != nil {
		a.logger.Error("Unable to store plain zettel/part in buffer", "err", err, "zid", zid)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if err = writeBuffer(w, &buf, contentType); err != nil {
		a.logger.Error("write plain data", "err", err, "zid", zid)
	}
}

func (a *API) writeSzData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
	case partMeta:
		obj = sexp.EncodeMetaRights(api.MetaRights{
			Meta:   z.Meta.Map(),
			Rights: a.getRights(ctx, z.Meta),
		})
	}
	if err = a.writeObject(w, zid, obj); err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("write sx data")
	}
}

func (a *API) writeEncodedZettelPart(
	ctx context.Context,
	w http.ResponseWriter, zn *ast.ZettelNode,
	enc api.EncodingEnum, encStr string, part partType,







|







132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
	case partMeta:
		obj = sexp.EncodeMetaRights(api.MetaRights{
			Meta:   z.Meta.Map(),
			Rights: a.getRights(ctx, z.Meta),
		})
	}
	if err = a.writeObject(w, zid, obj); err != nil {
		a.logger.Error("write sx data", "err", err, "zid", zid)
	}
}

func (a *API) writeEncodedZettelPart(
	ctx context.Context,
	w http.ResponseWriter, zn *ast.ZettelNode,
	enc api.EncodingEnum, encStr string, part partType,
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
		_, err = encdr.WriteZettel(&buf, zn)
	case partMeta:
		_, err = encdr.WriteMeta(&buf, zn.InhMeta)
	case partContent:
		_, err = encdr.WriteBlocks(&buf, &zn.BlocksAST)
	}
	if err != nil {
		a.log.Error().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if buf.Len() == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil {
		a.log.Error().Err(err).Zid(zn.Zid).Msg("Write Encoded Zettel")
	}
}







|









|


161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
		_, err = encdr.WriteZettel(&buf, zn)
	case partMeta:
		_, err = encdr.WriteMeta(&buf, zn.InhMeta)
	case partContent:
		_, err = encdr.WriteBlocks(&buf, &zn.BlocksAST)
	}
	if err != nil {
		a.logger.Error("Unable to store data in buffer", "err", err, "zid", zn.Zid)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if buf.Len() == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil {
		a.logger.Error("Write encoded zettel", "err", err, "zid", zn.Zid)
	}
}
Changes to internal/web/adapter/api/login.go.
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Login/free")
			}
			return
		}
		var token []byte
		if ident, cred := retrieveIdentCred(r); ident != "" {
			var err error
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
		}
		if len(token) == 0 {
			w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Login")
		}
	})
}

func retrieveIdentCred(r *http.Request) (string, string) {
	if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok {
		return ident, cred
	}
	if ident, cred, ok := r.BasicAuth(); ok {
		return ident, cred
	}
	return "", ""
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Refresh/free")
			}
			return
		}
		authData := a.getAuthData(ctx)
		if authData == nil || len(authData.Token) == 0 || authData.User == nil {
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := authData.Expires.Sub(authData.Issued)
		currentLifetime := authData.Now.Sub(authData.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil {
				a.log.Error().Err(err).Msg("Write old token")
			}
			return
		}

		// Token is a little bit aged. Create a new one
		token, err := a.getToken(authData.User)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Write renewed token")
		}
	})
}

func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
	return a.writeObject(w, id.Invalid, sx.MakeList(
		sx.MakeString("Bearer"),
		sx.MakeString(token),
		sx.Int64(int64(lifetime/time.Second)),
	))
}







|



















|




















|













|











|











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

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.logger.Error("Login/free", "err", err)
			}
			return
		}
		var token []byte
		if ident, cred := retrieveIdentCred(r); ident != "" {
			var err error
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
		}
		if len(token) == 0 {
			w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.logger.Error("Login", "err", err)
		}
	})
}

func retrieveIdentCred(r *http.Request) (string, string) {
	if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok {
		return ident, cred
	}
	if ident, cred, ok := r.BasicAuth(); ok {
		return ident, cred
	}
	return "", ""
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if !a.withAuth() {
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.logger.Error("Refresh/free", "err", err)
			}
			return
		}
		authData := a.getAuthData(ctx)
		if authData == nil || len(authData.Token) == 0 || authData.User == nil {
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := authData.Expires.Sub(authData.Issued)
		currentLifetime := authData.Now.Sub(authData.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil {
				a.logger.Error("write old token", "err", err)
			}
			return
		}

		// Token is a little bit aged. Create a new one
		token, err := a.getToken(authData.User)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.logger.Error("write renewed token", "err", err)
		}
	})
}

func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
	return a.writeObject(w, id.Invalid, sx.MakeList(
		sx.MakeString("Bearer"),
		sx.MakeString(token),
		sx.Int64(int64(lifetime/time.Second)),
	))
}
Changes to internal/web/adapter/api/query.go.
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		var buf bytes.Buffer
		err = queryAction(&buf, encoder, metaSeq, actions)
		if err != nil {
			a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}

		if err = writeBuffer(w, &buf, contentType); err != nil {
			a.log.Error().Err(err).Msg("write result buffer")
		}
	})
}
func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error {
	minVal, maxVal := -1, -1
	if len(actions) > 0 {
		acts := make([]string, 0, len(actions))







|




|







91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		var buf bytes.Buffer
		err = queryAction(&buf, encoder, metaSeq, actions)
		if err != nil {
			a.logger.Error("execute query action", "err", err, "query", sq)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}

		if err = writeBuffer(w, &buf, contentType); err != nil {
			a.logger.Error("write result buffer", "err", err)
		}
	})
}
func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error {
	minVal, maxVal := -1, -1
	if len(actions) > 0 {
		acts := make([]string, 0, len(actions))
296
297
298
299
300
301
302
303
304
305
	return true
}

func (a *API) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder, zid id.Zid) {
	w.Header().Set(api.HeaderContentType, content.PlainText)
	http.Redirect(w, r, ub.String(), http.StatusFound)
	if _, err := io.WriteString(w, zid.String()); err != nil {
		a.log.Error().Err(err).Msg("redirect body")
	}
}







|


296
297
298
299
300
301
302
303
304
305
	return true
}

func (a *API) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder, zid id.Zid) {
	w.Header().Set(api.HeaderContentType, content.PlainText)
	http.Redirect(w, r, ub.String(), http.StatusFound)
	if _, err := io.WriteString(w, zid.String()); err != nil {
		a.logger.Error("redirect body", "err", err)
	}
}
Changes to internal/web/adapter/api/request.go.
20
21
22
23
24
25
26

27
28
29
30
31
32
33

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sexp"
	"t73f.de/r/zsx/input"

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

// getEncoding returns the data encoding selected by the caller.
func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) {
	encoding := q.Get(api.QueryKeyEncoding)
	if encoding != "" {







>







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

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sexp"
	"t73f.de/r/zsx/input"

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

// getEncoding returns the data encoding selected by the caller.
func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) {
	encoding := q.Get(api.QueryKeyEncoding)
	if encoding != "" {
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
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()
	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()
	rdr := sxreader.MakeReader(r.Body)
	obj, err := rdr.Read()
	if err != nil {
		return zettel.Zettel{}, err
	}
	zd, err := sexp.ParseZettel(obj)
	if err != nil {







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

|













|







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
func getPart(q url.Values, defPart partType) partType {
	if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok {
		return part
	}
	return defPart
}




















func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	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 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/api/response.go.
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

	"zettelstore.de/z/internal/web/content"
)

func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, obj); err != nil {
		msg := a.log.Error().Err(err)
		if msg != nil {
			if zid.IsValid() {
				msg = msg.Zid(zid)
			}
			msg.Msg("Unable to store object in buffer")
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return nil
	}
	return writeBuffer(w, &buf, content.SXPF)
}







<
<
<
<
<
|
<





22
23
24
25
26
27
28





29

30
31
32
33
34

	"zettelstore.de/z/internal/web/content"
)

func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, obj); err != nil {





		a.logger.Error("Unable to store object in buffer", "err", err, "zid", zid)

		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return nil
	}
	return writeBuffer(w, &buf, content.SXPF)
}
Changes to internal/web/adapter/request.go.
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	"zettelstore.de/z/internal/query"
)

// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()
	if err != nil {
		kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form")
		return "", "", false
	}

	ident = strings.TrimSpace(r.PostFormValue("username"))
	cred = r.PostFormValue("password")
	if ident == "" {
		return "", "", false







|







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	"zettelstore.de/z/internal/query"
)

// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()
	if err != nil {
		kernel.Main.GetLogger(kernel.WebService).Info("Unable to parse form", "err", err)
		return "", "", false
	}

	ident = strings.TrimSpace(r.PostFormValue("username"))
	cred = r.PostFormValue("password")
	if ident == "" {
		return "", "", false
Changes to internal/web/adapter/webui/create_zettel.go.
21
22
23
24
25
26
27

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

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

	"zettelstore.de/z/internal/ast"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/web/server"
	"zettelstore.de/z/internal/zettel"
)

// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel,







>





<







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

34
35
36
37
38
39
40

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

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter"

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

// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel,
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
	w http.ResponseWriter,
	ztl zettel.Zettel,
	title string,
	formActionURL string,
	roleData []string,
	syntaxData []string,
) {
	user := server.GetUser(ctx)
	m := ztl.Meta

	var sb strings.Builder
	for key, val := range m.Rest() {
		sb.WriteString(key)
		sb.WriteString(": ")
		sb.WriteString(string(val))
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnv(ctx, "form", wui.getUserLang(ctx), title, user)
	rb.bindString("heading", sx.MakeString(title))
	rb.bindString("form-action-url", sx.MakeString(formActionURL))
	rb.bindString("role-data", makeStringList(roleData))
	rb.bindString("syntax-data", makeStringList(syntaxData))
	rb.bindString("meta", sx.MakeString(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.MakeString(ztl.Content.AsString()))







|









|







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
	w http.ResponseWriter,
	ztl zettel.Zettel,
	title string,
	formActionURL string,
	roleData []string,
	syntaxData []string,
) {
	user := user.GetCurrentUser(ctx)
	m := ztl.Meta

	var sb strings.Builder
	for key, val := range m.Rest() {
		sb.WriteString(key)
		sb.WriteString(": ")
		sb.WriteString(string(val))
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnvironment(ctx, "form", wui.getUserLang(ctx), title, user)
	rb.bindString("heading", sx.MakeString(title))
	rb.bindString("form-action-url", sx.MakeString(formActionURL))
	rb.bindString("role-data", makeStringList(roleData))
	rb.bindString("syntax-data", makeStringList(syntaxData))
	rb.bindString("meta", sx.MakeString(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.MakeString(ztl.Content.AsString()))
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
		reEdit, zettel, err := parseZettelForm(r, id.Invalid)
		if err == errMissingContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
			return
		}
		if err != nil {
			const msg = "Unable to read form data"
			wui.log.Info().Err(err).Msg(msg)
			wui.reportError(ctx, w, adapter.NewErrBadRequest(msg))
			return
		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			wui.reportError(ctx, w, err)







|







136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
		reEdit, zettel, err := parseZettelForm(r, id.Invalid)
		if err == errMissingContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
			return
		}
		if err != nil {
			const msg = "Unable to read form data"
			wui.logger.Info(msg, "err", err)
			wui.reportError(ctx, w, adapter.NewErrBadRequest(msg))
			return
		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			wui.reportError(ctx, w, err)
Changes to internal/web/adapter/webui/delete_zettel.go.
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
	"slices"

	"t73f.de/r/sx"
	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/server"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(
	getZettel usecase.GetZettel,
	getAllZettel usecase.GetAllZettel,







|
|
|







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

	"t73f.de/r/sx"
	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/usecase"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(
	getZettel usecase.GetZettel,
	getAllZettel usecase.GetAllZettel,
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.MakeString(string(zs[1].Meta.GetDefault(meta.KeyBoxNumber, "???"))))
			rb.bindString("incoming", nil)
		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))







|
|







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := zs[0].Meta

		user := user.GetCurrentUser(ctx)
		env, rb := wui.createRenderEnvironment(
			ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user)
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.MakeString(string(zs[1].Meta.GetDefault(meta.KeyBoxNumber, "???"))))
			rb.bindString("incoming", nil)
		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
Changes to internal/web/adapter/webui/edit_zettel.go.
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
		}

		reEdit, zettel, err := parseZettelForm(r, zid)
		hasContent := true
		if err != nil {
			if err != errMissingContent {
				const msg = "Unable to read zettel form"
				wui.log.Info().Err(err).Msg(msg)
				wui.reportError(ctx, w, adapter.NewErrBadRequest(msg))
				return
			}
			hasContent = false
		}
		if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
			wui.reportError(ctx, w, err)







|







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
		}

		reEdit, zettel, err := parseZettelForm(r, zid)
		hasContent := true
		if err != nil {
			if err != errMissingContent {
				const msg = "Unable to read zettel form"
				wui.logger.Info(msg, "err", err)
				wui.reportError(ctx, w, adapter.NewErrBadRequest(msg))
				return
			}
			hasContent = false
		}
		if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
			wui.reportError(ctx, w, err)
Changes to internal/web/adapter/webui/favicon.go.
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

// MakeFaviconHandler creates a HTTP handler to retrieve the favicon.
func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			wui.log.Debug().Err(err).Msg("Favicon not found")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		defer f.Close()

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

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







|



|



|





|



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

// MakeFaviconHandler creates a HTTP handler to retrieve the favicon.
func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			wui.logger.Debug("Favicon not found", "err", err)
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		defer func() { _ = f.Close() }()

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

		if err = adapter.WriteData(w, data, ""); err != nil {
			wui.logger.Error("Write favicon", "err", err)
		}
	})
}
Changes to internal/web/adapter/webui/forms.go.
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()
		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]







|







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 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/get_info.go.
21
22
23
24
25
26
27

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

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

	"zettelstore.de/z/internal/ast"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/server"
	"zettelstore.de/z/strfun"
)

// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(
	ucParseZettel usecase.ParseZettel,
	ucGetReferences usecase.GetReferences,







>





<







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

34
35
36
37
38
39
40

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

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/usecase"

	"zettelstore.de/z/strfun"
)

// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(
	ucParseZettel usecase.ParseZettel,
	ucGetReferences usecase.GetReferences,
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
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		encTexts := encodingTexts()
		shadowLinks := getShadowLinks(ctx, zid, zn.InhMeta.GetDefault(meta.KeyBoxNumber, ""), ucGetAllZettel)

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "info", wui.getUserLang(ctx), title, user)
		rb.bindString("metadata", lbMetadata.List())
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.MakeString(phrase))
		rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)

		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZidInfoTemplate, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)







|
|











>







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
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		encTexts := encodingTexts()
		shadowLinks := getShadowLinks(ctx, zid, zn.InhMeta.GetDefault(meta.KeyBoxNumber, ""), ucGetAllZettel)

		user := user.GetCurrentUser(ctx)
		env, rb := wui.createRenderEnvironment(ctx, "info", wui.getUserLang(ctx), title, user)
		rb.bindString("metadata", lbMetadata.List())
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.MakeString(phrase))
		rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		rb.bindString("version-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZidInfoTemplate, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
Changes to internal/web/adapter/webui/get_zettel.go.
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36
37
38
39
	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/server"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(
	evaluate *usecase.Evaluate,
	getZettel usecase.GetZettel,
) http.Handler {







>



<







22
23
24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/usecase"

)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(
	evaluate *usecase.Evaluate,
	getZettel usecase.GetZettel,
) http.Handler {
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
		metaObj := enc.MetaSxn(zn.InhMeta)
		content, endnotes, err := enc.BlocksSxn(&zn.BlocksAST)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := server.GetUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := ast.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnv(ctx, "zettel", zettelLang, title, user)
		rb.bindSymbol(symMetaHeader, metaObj)
		rb.bindString("heading", sx.MakeString(title))
		if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" {
			rb.bindString(
				"role-url",
				sx.MakeString(wui.NewURLBuilder('h').AppendQuery(
					meta.KeyRole+api.SearchOperatorHas+string(role)).String()))







|



|







58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
		metaObj := enc.MetaSxn(zn.InhMeta)
		content, endnotes, err := enc.BlocksSxn(&zn.BlocksAST)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := user.GetCurrentUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := ast.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnvironment(ctx, "zettel", zettelLang, title, user)
		rb.bindSymbol(symMetaHeader, metaObj)
		rb.bindString("heading", sx.MakeString(title))
		if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" {
			rb.bindString(
				"role-url",
				sx.MakeString(wui.NewURLBuilder('h').AppendQuery(
					meta.KeyRole+api.SearchOperatorHas+string(role)).String()))
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
		wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, meta.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "sequel", zn.InhMeta, meta.KeySequel, config.KeyShowSequelLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, meta.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "back", zn.InhMeta, meta.KeyBack, config.KeyShowBackLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, meta.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle)
		if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" {
			for _, part := range []string{"meta", "actions", "heading"} {
				rb.rebindResolved("ROLE-"+string(role)+"-"+part, "ROLE-DEFAULT-"+part)
			}
		}
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZidZettelTemplate, env)
		} else {
			err = rb.err







|







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
		wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, meta.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "sequel", zn.InhMeta, meta.KeySequel, config.KeyShowSequelLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, meta.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "back", zn.InhMeta, meta.KeyBack, config.KeyShowBackLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, meta.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle)
		if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" {
			for _, part := range []string{"meta", "actions", "heading"} {
				rb.rebindResolved("ROLE-"+string(role)+"-"+part, "ROLE-EXTRA-"+part)
			}
		}
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZidZettelTemplate, env)
		} else {
			err = rb.err
Changes to internal/web/adapter/webui/home.go.
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
import (
	"context"
	"errors"
	"net/http"

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


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

type getRootPort interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}








>



<







16
17
18
19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
import (
	"context"
	"errors"
	"net/http"

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

	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/web/adapter"

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

type getRootPort interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

48
49
50
51
52
53
54
55
56
57
58
59
60
61
			homeZid = id.ZidDefaultHome
		}
		_, err := s.GetZettel(ctx, homeZid)
		if err == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
			return
		}
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('i'))
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('h'))
	})
}







|






48
49
50
51
52
53
54
55
56
57
58
59
60
61
			homeZid = id.ZidDefaultHome
		}
		_, err := s.GetZettel(ctx, homeZid)
		if err == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid))
			return
		}
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && user.GetCurrentUser(ctx) == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('i'))
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('h'))
	})
}
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
	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
	}







<
<
<
<
<
<
<
<







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
}









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
		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(space)
		}
		lb.Add(wui.transformKeyValueText(key, meta.Value(tag), tag))
	}
	if len(tags) > 1 {
		lb.AddN(space, wui.transformKeyValuesText(key, tags, "(all)"))
	}







<

>
>
|







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

	for i, tag := range tags {
		if i == 0 {
			lb.Add(shtml.SymSPAN)
		} 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/lists.go.
25
26
27
28
29
30
31

32
33
34
35
36
37
38
39
40
41
42
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"

	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/web/server"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,







>



<







25
26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter"

)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}

		siteName := wui.rtConfig.GetSiteName()
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "list", userLang, siteName, user)
		if q == nil {
			rb.bindString("heading", sx.MakeString(siteName))
		} else {
			var sb strings.Builder
			q.PrintHuman(&sb)
			rb.bindString("heading", sx.MakeString(sb.String()))
		}







|
|







79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}

		siteName := wui.rtConfig.GetSiteName()
		user := user.GetCurrentUser(ctx)
		env, rb := wui.createRenderEnvironment(ctx, "list", userLang, siteName, user)
		if q == nil {
			rb.bindString("heading", sx.MakeString(siteName))
		} else {
			var sb strings.Builder
			q.PrintHuman(&sb)
			rb.bindString("heading", sx.MakeString(sb.String()))
		}
Changes to internal/web/adapter/webui/login.go.
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
			return
		}
		wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
	})
}

func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
	env, rb := wui.createRenderEnv(ctx, "login", wui.getUserLang(ctx), "Login", nil)
	rb.bindString("retry", sx.MakeBoolean(retry))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.ZidLoginTemplate, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}







|







36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
			return
		}
		wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
	})
}

func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
	env, rb := wui.createRenderEnvironment(ctx, "login", wui.getUserLang(ctx), "Login", nil)
	rb.bindString("retry", sx.MakeBoolean(retry))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.ZidLoginTemplate, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}
Changes to internal/web/adapter/webui/response.go.
17
18
19
20
21
22
23
24
25
26
	"net/http"

	"t73f.de/r/zsc/api"
)

func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
	us := ub.String()
	wui.log.Debug().Str("uri", us).Msg("redirect")
	http.Redirect(w, r, us, http.StatusFound)
}







|


17
18
19
20
21
22
23
24
25
26
	"net/http"

	"t73f.de/r/zsc/api"
)

func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
	us := ub.String()
	wui.logger.Debug("redirect", "uri", us)
	http.Redirect(w, r, us, http.StatusFound)
}
Changes to internal/web/adapter/webui/sxn_code.go.
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
		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() {







<







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.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() {
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
}

func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error {
	rdr, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return err
	}
	env := sxeval.MakeEnvironment()
	for {
		form, err2 := rdr.Read()
		if err2 != nil {
			if err2 == io.EOF {
				return nil
			}
			return err2
		}
		wui.log.Debug().Zid(zid).Str("form", form.String()).Msg("Loaded sxn code")

		if _, err2 = env.Eval(form, bind); err2 != nil {
			return err2
		}
	}
}







|








|

|




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

func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error {
	rdr, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return err
	}
	env := sxeval.MakeEnvironment(bind)
	for {
		form, err2 := rdr.Read()
		if err2 != nil {
			if err2 == io.EOF {
				return nil
			}
			return err2
		}
		wui.logger.Debug("Loaded sxn code", "zid", zid, "form", form)

		if _, err2 = env.Eval(form, nil); err2 != nil {
			return err2
		}
	}
}
Changes to internal/web/adapter/webui/template.go.
13
14
15
16
17
18
19

20
21

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

34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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

package webui

import (
	"bytes"
	"context"
	"fmt"

	"net/http"
	"net/url"


	"t73f.de/r/sx"
	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/collect"
	"zettelstore.de/z/internal/config"
	"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)

	_ = 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) {
				text, err := sxbuiltins.GetString(arg, 0)
				if err != nil {
					return nil, err
				}
				return wui.url2html(text), nil
			},
		},
		&sxeval.Builtin{
			Name:     "zid-content-path",
			MinArity: 1,
			MaxArity: 1,
			TestPure: sxeval.AssertPure,
			Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Binding) (sx.Object, error) {
				s, err := sxbuiltins.GetString(arg, 0)
				if err != nil {
					return nil, err
				}
				zid, err := id.Parse(s.GetValue())
				if err != nil {
					return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err)
				}
				ub := wui.NewURLBuilder('z').SetZid(zid)
				return sx.MakeString(ub.String()), nil
			},
		},
		&sxeval.Builtin{
			Name:     "query->url",
			MinArity: 1,
			MaxArity: 1,
			TestPure: sxeval.AssertPure,
			Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Binding) (sx.Object, error) {
				qs, err := sxbuiltins.GetString(arg, 0)
				if err != nil {
					return nil, err
				}
				u := wui.NewURLBuilder('h').AppendQuery(qs.GetValue())
				return sx.MakeString(u.String()), nil
			},
		})
	root.Freeze()
	return root
}

var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefVarS,                     // defvar
		&sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda
		&sxbuiltins.SetXS,     // set!
		&sxbuiltins.IfS,       // if
		&sxbuiltins.BeginS,    // begin
		&sxbuiltins.DefMacroS, // defmacro
		&sxbuiltins.LetS,      // let

	}
	builtins = []*sxeval.Builtin{
		&sxbuiltins.Equal,                // =
		&sxbuiltins.NumGreater,           // >
		&sxbuiltins.NullP,                // null?
		&sxbuiltins.PairP,                // pair?
		&sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr
		&sxbuiltins.Caar, &sxbuiltins.Cadr, &sxbuiltins.Cdar, &sxbuiltins.Cddr,
		&sxbuiltins.Caaar, &sxbuiltins.Caadr, &sxbuiltins.Cadar, &sxbuiltins.Caddr,
		&sxbuiltins.Cdaar, &sxbuiltins.Cdadr, &sxbuiltins.Cddar, &sxbuiltins.Cdddr,
		&sxbuiltins.List,           // list
		&sxbuiltins.Append,         // append
		&sxbuiltins.Assoc,          // assoc
		&sxbuiltins.Map,            // map
		&sxbuiltins.Apply,          // apply
		&sxbuiltins.Concat,         // concat
		&sxbuiltins.BoundP,         // bound?
		&sxbuiltins.DefinedP,       // defined?
		&sxbuiltins.CurrentBinding, // current-binding
		&sxbuiltins.BindingLookup,  // binding-lookup
	}
)

func (wui *WebUI) url2html(text sx.String) sx.Object {
	if u, errURL := url.Parse(text.GetValue()); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				shtml.SymA,
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(shtml.SymAttrHref, sx.MakeString(us)),
					sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")),
					sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")),
				),
				text)
		}
	}
	return text
}

func (wui *WebUI) getParentEnv(ctx context.Context) (*sxeval.Binding, error) {
	wui.mxZettelBinding.Lock()
	defer wui.mxZettelBinding.Unlock()
	if parentEnv := wui.zettelBinding; parentEnv != nil {
		return parentEnv, nil
	}
	dag, zettelEnv, err := wui.loadAllSxnCodeZettel(ctx)
	if err != nil {
		wui.log.Error().Err(err).Msg("loading zettel sxn")
		return nil, err
	}
	wui.dag = dag
	wui.zettelBinding = zettelEnv
	return zettelEnv, nil
}


// createRenderEnv creates a new environment and populates it with all relevant data for the base template.
func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Binding, renderBinder) {
	userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)
	parentEnv, err := wui.getParentEnv(ctx)
	bind := parentEnv.MakeChildBinding(name, 128)
	rb := makeRenderBinder(bind, err)
	rb.bindString("lang", sx.MakeString(lang))
	rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL))
	rb.bindString("title", sx.MakeString(title))
	rb.bindString("home-url", sx.MakeString(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))







>


>












>



|
|
|



|
|
>


<
<






|












|

















|


















|
|
|
|
|
>










|
|
|
|
|
|
|
|
|
|




















|


|
|

|

|



|
|


>
|
|

|
|







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


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181

package webui

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/collect"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/logging"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/zettel"
)

func (wui *WebUI) createRootBinding() *sxeval.Binding {
	root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 32)
	_ = sxbuiltins.LoadPrelude(root)
	_ = sxeval.BindSpecials(root, specials...)
	_ = sxeval.BindBuiltins(root, builtins...)


	_ = sxeval.BindBuiltins(root,
		&sxeval.Builtin{
			Name:     "url-to-html",
			MinArity: 1,
			MaxArity: 1,
			TestPure: sxeval.AssertPure,
			Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Frame) (sx.Object, error) {
				text, err := sxbuiltins.GetString(arg, 0)
				if err != nil {
					return nil, err
				}
				return wui.url2html(text), nil
			},
		},
		&sxeval.Builtin{
			Name:     "zid-content-path",
			MinArity: 1,
			MaxArity: 1,
			TestPure: sxeval.AssertPure,
			Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Frame) (sx.Object, error) {
				s, err := sxbuiltins.GetString(arg, 0)
				if err != nil {
					return nil, err
				}
				zid, err := id.Parse(s.GetValue())
				if err != nil {
					return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err)
				}
				ub := wui.NewURLBuilder('z').SetZid(zid)
				return sx.MakeString(ub.String()), nil
			},
		},
		&sxeval.Builtin{
			Name:     "query->url",
			MinArity: 1,
			MaxArity: 1,
			TestPure: sxeval.AssertPure,
			Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Frame) (sx.Object, error) {
				qs, err := sxbuiltins.GetString(arg, 0)
				if err != nil {
					return nil, err
				}
				u := wui.NewURLBuilder('h').AppendQuery(qs.GetValue())
				return sx.MakeString(u.String()), nil
			},
		})
	root.Freeze()
	return root
}

var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefVarS,                     // defvar
		&sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda
		&sxbuiltins.SetXS,                      // set!
		&sxbuiltins.IfS,                        // if
		&sxbuiltins.BeginS,                     // begin
		&sxbuiltins.DefMacroS,                  // defmacro
		&sxbuiltins.LetS, &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
		&sxbuiltins.Caar, &sxbuiltins.Cadr, &sxbuiltins.Cdar, &sxbuiltins.Cddr,
		&sxbuiltins.Caaar, &sxbuiltins.Caadr, &sxbuiltins.Cadar, &sxbuiltins.Caddr,
		&sxbuiltins.Cdaar, &sxbuiltins.Cdadr, &sxbuiltins.Cddar, &sxbuiltins.Cdddr,
		&sxbuiltins.List,          // list
		&sxbuiltins.Append,        // append
		&sxbuiltins.Assoc,         // assoc
		&sxbuiltins.Map,           // map
		&sxbuiltins.Apply,         // apply
		&sxbuiltins.Concat,        // concat
		&sxbuiltins.SymbolBoundP,  // symbol-bound?
		&sxbuiltins.DefinedP,      // defined?
		&sxbuiltins.CurrentFrame,  // current-frame
		&sxbuiltins.ResolveSymbol, // resolve-symbol
	}
)

func (wui *WebUI) url2html(text sx.String) sx.Object {
	if u, errURL := url.Parse(text.GetValue()); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				shtml.SymA,
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(shtml.SymAttrHref, sx.MakeString(us)),
					sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")),
					sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")),
				),
				text)
		}
	}
	return text
}

func (wui *WebUI) getParentBinding(ctx context.Context) (*sxeval.Binding, error) {
	wui.mxZettelBinding.Lock()
	defer wui.mxZettelBinding.Unlock()
	if parentBind := wui.zettelBinding; parentBind != nil {
		return parentBind, nil
	}
	dag, zettelBind, err := wui.loadAllSxnCodeZettel(ctx)
	if err != nil {
		wui.logger.Error("loading zettel sxn", "err", err)
		return nil, err
	}
	wui.dag = dag
	wui.zettelBinding = zettelBind
	return zettelBind, nil
}

// createRenderEnvironment creates a new environment and populates it with all
// relevant data for the base template.
func (wui *WebUI) createRenderEnvironment(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Environment, renderBinder) {
	userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)
	parentBind, err := wui.getParentBinding(ctx)
	bind := parentBind.MakeChildBinding(name, 128)
	rb := makeRenderBinder(bind, err)
	rb.bindString("lang", sx.MakeString(lang))
	rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL))
	rb.bindString("title", sx.MakeString(title))
	rb.bindString("home-url", sx.MakeString(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
188
189
190
191
192
193
194








195
196
197
198
199
200
201
202
203
204
































205
206
207
208
209
210
211
	rb.bindString("search-url", sx.MakeString(wui.searchURL))
	rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed))
	rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
	rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
	rb.bindSymbol(symMetaHeader, sx.Nil())
	rb.bindSymbol(symDetail, sx.Nil())








	return bind, rb
}

func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) {
	if user == nil {
		return false, "", ""
	}
	return true, wui.NewURLBuilder('h').SetZid(user.Zid).String(), string(user.GetDefault(meta.KeyUserID, ""))
}

































type renderBinder struct {
	err     error
	binding *sxeval.Binding
}

func makeRenderBinder(bind *sxeval.Binding, err error) renderBinder {
	return renderBinder{binding: bind, err: err}







>
>
>
>
>
>
>
>
|









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







192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
	rb.bindString("search-url", sx.MakeString(wui.searchURL))
	rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed))
	rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
	rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
	rb.bindSymbol(symMetaHeader, sx.Nil())
	rb.bindSymbol(symDetail, sx.Nil())

	nestH := sxeval.MakeNestingLimitHandler(wui.sxMaxNesting, sxeval.DefaultHandler{})
	var handler sxeval.ComputeHandler = nestH
	if logger := wui.logger; logger.Handler().Enabled(context.Background(), logging.LevelTrace) {
		stepsH := sxeval.MakeStepsHandler(nestH)
		handler = &computeLogHandler{logger: logger, nest: nestH, next: stepsH}
	}
	env := sxeval.MakeEnvironment(bind).SetComputeHandler(handler)
	return env, rb
}

func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) {
	if user == nil {
		return false, "", ""
	}
	return true, wui.NewURLBuilder('h').SetZid(user.Zid).String(), string(user.GetDefault(meta.KeyUserID, ""))
}

type computeLogHandler struct {
	logger *slog.Logger
	nest   *sxeval.NestingLimitHandler
	next   *sxeval.StepsHandler
}

func (clh *computeLogHandler) Compute(env *sxeval.Environment, expr sxeval.Expr, frame *sxeval.Frame) (sx.Object, error) {
	fname := "nil"
	if frame != nil {
		fname = frame.Name()
	}
	curNesting, _ := clh.nest.Nesting()
	var sb strings.Builder
	_, _ = expr.Print(&sb)
	logging.LogTrace(clh.logger, "compute",
		slog.String("frame", fname),
		slog.Int("steps", clh.next.Steps),
		slog.Int("level", curNesting),
		slog.String("expr", sb.String()))
	obj, err := clh.next.Compute(env, expr, frame)
	if err == nil {
		logging.LogTrace(clh.logger, "result ",
			slog.String("frame", fname),
			slog.Int("steps", clh.next.Steps),
			slog.Int("level", curNesting),
			slog.Any("object", obj))
	}
	return obj, err
}

func (clh *computeLogHandler) Reset() { clh.next.Reset() }

type renderBinder struct {
	err     error
	binding *sxeval.Binding
}

func makeRenderBinder(bind *sxeval.Binding, err error) renderBinder {
	return renderBinder{binding: bind, err: err}
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
}
func (rb *renderBinder) bindKeyValue(key string, value meta.Value) {
	rb.bindString("meta-"+key, sx.MakeString(string(value)))
	if kt := meta.Type(key); kt.IsSet {
		rb.bindString("set-meta-"+key, makeStringList(value.AsSlice()))
	}
}
func (rb *renderBinder) rebindResolved(key, defKey string) {
	if rb.err == nil {
		if obj, found := rb.binding.Resolve(sx.MakeSymbol(key)); found {


			rb.bindString(defKey, obj)


		}
	}
}

func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
	zid := m.Zid
	strZid := zid.String()
	newURLBuilder := wui.NewURLBuilder

	rb.bindString("zid", sx.MakeString(strZid))
	rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(zid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(zid).String()))
	}
	rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(zid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}
		rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		rb.bindString("sequel-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionSequel).String()))
		rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(zid).String()))
	}
	if val, found := m.Get(meta.KeyUselessFiles); found {







|

|
>
>
|
>
>



















<







266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299

300
301
302
303
304
305
306
}
func (rb *renderBinder) bindKeyValue(key string, value meta.Value) {
	rb.bindString("meta-"+key, sx.MakeString(string(value)))
	if kt := meta.Type(key); kt.IsSet {
		rb.bindString("set-meta-"+key, makeStringList(value.AsSlice()))
	}
}
func (rb *renderBinder) rebindResolved(key, extraKey string) {
	if rb.err == nil {
		sym := sx.MakeSymbol(key)
		for curr := rb.binding; curr != nil; curr = curr.Parent() {
			if obj, found := curr.Lookup(sym); found {
				rb.bindString(extraKey, obj)
				return
			}
		}
	}
}

func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
	zid := m.Zid
	strZid := zid.String()
	newURLBuilder := wui.NewURLBuilder

	rb.bindString("zid", sx.MakeString(strZid))
	rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(zid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(zid).String()))
	}
	rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(zid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}

		rb.bindString("sequel-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionSequel).String()))
		rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(zid).String()))
	}
	if val, found := m.Get(meta.KeyUselessFiles); found {
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412

413

414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
				return content
			}
		}
	}
	return nil
}

func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sxeval.Expr, error) {
	if t := wui.getSxnCache(zid); t != nil {
		return t, nil
	}

	reader, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return nil, err
	}

	objs, err := reader.ReadAll()
	if err != nil {
		wui.log.Error().Err(err).Zid(zid).Msg("reading sxn template")
		return nil, err
	}
	if len(objs) != 1 {
		return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs))
	}
	env := sxeval.MakeEnvironment()
	t, err := env.Parse(objs[0], bind)
	if err != nil {
		return nil, err
	}

	wui.setSxnCache(zid, t)
	return t, nil
}
func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) {
	ztl, err := wui.box.GetZettel(ctx, zid)
	if err != nil {
		return nil, err
	}

	reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()))
	return reader, nil
}

func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sx.Object, error) {
	templateExpr, err := wui.getSxnTemplate(ctx, zid, bind)
	if err != nil {
		return nil, err
	}
	env := sxeval.MakeEnvironment()
	return env.Run(templateExpr, bind)
}

func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, bind *sxeval.Binding) error {
	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)



	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.
		msg.Str("page", pageObj.String()).Msg("render")
	}

	gen := sxhtml.NewGenerator().SetNewline()
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	if _, err = w.Write(sb.Bytes()); err != nil {
		wui.log.Error().Err(err).Msg("Unable to write HTML via template")
	}
	return nil // No error reporting, since we do not know what happended during write to client.
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	ctx = context.WithoutCancel(ctx) // Ignore any cancel / timeouts to write an error message.
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	} else {
		wui.log.Debug().Err(err).Msg("reportError")
	}
	user := server.GetUser(ctx)
	env, rb := wui.createRenderEnv(ctx, "error", meta.ValueLangEN, "Error", user)
	rb.bindString("heading", sx.MakeString(http.StatusText(code)))
	rb.bindString("message", sx.MakeString(text))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ZidErrorTemplate, env)
	}
	errSx := rb.err
	if errSx == nil {
		return
	}
	wui.log.Error().Err(errSx).Msg("while rendering error message")

	// if errBind != nil, the HTTP header was not written
	wui.prepareAndWriteHeader(w, http.StatusInternalServerError)
	fmt.Fprintf(
		w,
		`<!DOCTYPE html>
<html>
<head><title>Internal server error</title></head>
<body>
<h1>Internal server error</h1>
<p>When generating error code %d with message:</p><pre>%v</pre><p>an error occured:</p><pre>%v</pre>







|











|





<
|

















|
|



<
|


|
|

|
|



|
>
|
>
|



<
<
|
<









|








|

|

|
|



|





|



|







398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422

423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445

446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464


465

466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
				return content
			}
		}
	}
	return nil
}

func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env *sxeval.Environment) (sxeval.Expr, error) {
	if t := wui.getSxnCache(zid); t != nil {
		return t, nil
	}

	reader, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return nil, err
	}

	objs, err := reader.ReadAll()
	if err != nil {
		wui.logger.Error("reading sxn template", "err", err, "zid", zid)
		return nil, err
	}
	if len(objs) != 1 {
		return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs))
	}

	t, err := env.Parse(objs[0], nil)
	if err != nil {
		return nil, err
	}

	wui.setSxnCache(zid, t)
	return t, nil
}
func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) {
	ztl, err := wui.box.GetZettel(ctx, zid)
	if err != nil {
		return nil, err
	}

	reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()))
	return reader, nil
}

func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env *sxeval.Environment) (sx.Object, error) {
	templateExpr, err := wui.getSxnTemplate(ctx, zid, env)
	if err != nil {
		return nil, err
	}

	return env.Run(templateExpr, nil)
}

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

	pageObj, err := wui.evalSxnTemplate(ctx, id.ZidBaseTemplate, env)
	if err != nil {
		return err
	}


	wui.logger.Debug("render", "page", pageObj)


	gen := sxhtml.NewGenerator().SetNewline()
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	if _, err = w.Write(sb.Bytes()); err != nil {
		wui.logger.Error("Unable to write HTML via template", "err", err)
	}
	return nil // No error reporting, since we do not know what happended during write to client.
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	ctx = context.WithoutCancel(ctx) // Ignore any cancel / timeouts to write an error message.
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.logger.Error(err.Error())
	} else {
		wui.logger.Debug("reportError", "err", err)
	}
	user := user.GetCurrentUser(ctx)
	bind, rb := wui.createRenderEnvironment(ctx, "error", meta.ValueLangEN, "Error", user)
	rb.bindString("heading", sx.MakeString(http.StatusText(code)))
	rb.bindString("message", sx.MakeString(text))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ZidErrorTemplate, bind)
	}
	errSx := rb.err
	if errSx == nil {
		return
	}
	wui.logger.Error("while rendering error message", "err", errSx)

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


19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//-----------------------------------------------------------------------------

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"


	"net/http"
	"sync"
	"time"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zero/graph"
	"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/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"
	"zettelstore.de/z/internal/logger"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/web/server"
	"zettelstore.de/z/internal/zettel"
)

// WebUI holds all data for delivering the web ui.
type WebUI struct {
	log      *logger.Logger
	debug    bool
	ab       server.AuthBuilder
	authz    auth.AuthzManager
	rtConfig config.Config
	token    auth.TokenManager
	box      webuiBox
	policy   auth.Policy







>
>
















<








|







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

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"context"
	"log/slog"
	"math"
	"net/http"
	"sync"
	"time"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zero/graph"
	"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/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/kernel"

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

// WebUI holds all data for delivering the web ui.
type WebUI struct {
	logger   *slog.Logger
	debug    bool
	ab       server.AuthBuilder
	authz    auth.AuthzManager
	rtConfig config.Config
	token    auth.TokenManager
	box      webuiBox
	policy   auth.Policy
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
	refreshURL    string
	withAuth      bool
	loginURL      string
	logoutURL     string
	searchURL     string
	createNewURL  string


	rootBinding     *sxeval.Binding
	mxZettelBinding sync.Mutex
	zettelBinding   *sxeval.Binding
	dag             graph.Digraph[id.Zid]
	genHTML         *sxhtml.Generator
}

// webuiBox contains all box methods that are needed for WebUI operation.
//
// Note: these function must not do auth checking.
type webuiBox interface {
	CanCreateZettel(context.Context) bool
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	CanUpdateZettel(context.Context, zettel.Zettel) bool
	CanDeleteZettel(context.Context, id.Zid) bool
}

// New creates a new WebUI struct.
func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI {
	loginoutBase := ab.NewURLBuilder('i')

	wui := &WebUI{
		log:      log,
		debug:    kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,







>



















|




|







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
	refreshURL    string
	withAuth      bool
	loginURL      string
	logoutURL     string
	searchURL     string
	createNewURL  string

	sxMaxNesting    int
	rootBinding     *sxeval.Binding
	mxZettelBinding sync.Mutex
	zettelBinding   *sxeval.Binding
	dag             graph.Digraph[id.Zid]
	genHTML         *sxhtml.Generator
}

// webuiBox contains all box methods that are needed for WebUI operation.
//
// Note: these function must not do auth checking.
type webuiBox interface {
	CanCreateZettel(context.Context) bool
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	CanUpdateZettel(context.Context, zettel.Zettel) bool
	CanDeleteZettel(context.Context, id.Zid) bool
}

// New creates a new WebUI struct.
func New(logger *slog.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI {
	loginoutBase := ab.NewURLBuilder('i')

	wui := &WebUI{
		logger:   logger,
		debug:    kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,
110
111
112
113
114
115
116

117
118
119
120
121
122
123
124
125
126
127
		refreshURL:    ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(),
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),


		zettelBinding: nil,
		genHTML:       sxhtml.NewGenerator().SetNewline(),
	}
	wui.rootBinding = wui.createRenderBinding()
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) getConfig(ctx context.Context, m *meta.Meta, key string) string {
	return wui.rtConfig.Get(ctx, m, key)







>



|







112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
		refreshURL:    ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(),
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),

		sxMaxNesting:  min(max(kernel.Main.GetConfig(kernel.WebService, kernel.WebSxMaxNesting).(int), 0), math.MaxInt),
		zettelBinding: nil,
		genHTML:       sxhtml.NewGenerator().SetNewline(),
	}
	wui.rootBinding = wui.createRootBinding()
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) getConfig(ctx context.Context, m *meta.Meta, key string) string {
	return wui.rtConfig.Get(ctx, m, key)
Changes to internal/web/server/http.go.
11
12
13
14
15
16
17

18
19
20
21



22
23

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


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


64
65
66









67
68
69
70
71
72
73
74
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package server

import (
	"context"

	"net"
	"net/http"
	"time"




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

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

type webServer struct {
	log              *logger.Logger
	baseURL          string
	httpServer       httpServer
	router           httpRouter
	persistentCookie bool
	secureCookie     bool
}

// ConfigData contains the data needed to configure a server.
type ConfigData struct {
	Log              *logger.Logger
	ListenAddr       string
	BaseURL          string
	URLPrefix        string
	MaxRequestSize   int64
	Auth             auth.TokenManager


	PersistentCookie bool
	SecureCookie     bool
	Profiling        bool
}

// New creates a new web server.
func New(sd 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,


		profiling:      sd.Profiling,
	}
	srv.router.initializeRouter(rd)









	srv.httpServer.initializeHTTPServer(sd.ListenAddr, &srv.router)
	return &srv
}

func (srv *webServer) Handle(pattern string, handler http.Handler) {
	srv.router.Handle(pattern, handler)
}
func (srv *webServer) AddListRoute(key byte, method Method, handler http.Handler) {







>




>
>
>

|
>

|



|









|





>
>



















>
>



>
>
>
>
>
>
>
>
>
|







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

package server

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

	"t73f.de/r/webs/middleware"
	"t73f.de/r/webs/middleware/logging"
	"t73f.de/r/webs/middleware/reqid"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"

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

type webServer struct {
	log              *slog.Logger
	baseURL          string
	httpServer       httpServer
	router           httpRouter
	persistentCookie bool
	secureCookie     bool
}

// ConfigData contains the data needed to configure a server.
type ConfigData struct {
	Log              *slog.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)

	mwReqID := reqid.Config{WithContext: true}
	mwLogReq := logging.ReqConfig{
		Logger: sd.Log, Level: slog.LevelDebug,
		Message: "ServeHTTP", WithRequestID: true, WithRemote: true}
	mwLogResp := logging.RespConfig{Logger: sd.Log, Level: slog.LevelDebug,
		Message: "/ServeHTTP", WithRequestID: true}
	mw := middleware.NewChain(mwReqID.Build(), mwLogReq.Build(), mwLogResp.Build())

	srv.httpServer.initializeHTTPServer(sd.ListenAddr, middleware.Apply(mw, &srv.router))
	return &srv
}

func (srv *webServer) Handle(pattern string, handler http.Handler) {
	srv.router.Handle(pattern, handler)
}
func (srv *webServer) AddListRoute(key byte, method Method, handler http.Handler) {
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
		Secure:   srv.secureCookie,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	}
	if srv.persistentCookie && d > 0 {
		cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC()
	}
	srv.log.Debug().Bytes("token", token).Msg("SetToken")
	if v := cookie.String(); v != "" {
		w.Header().Add("Set-Cookie", v)
		w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`)
		w.Header().Add("Vary", "Cookie")
	}
}

// ClearToken invalidates the session cookie by sending an empty one.
func (srv *webServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context {
	if authData := GetAuthData(ctx); authData == nil {
		// No authentication data stored in session, nothing to do.
		return ctx
	}
	if w != nil {
		srv.SetToken(w, nil, 0)
	}
	return updateContext(ctx, nil, nil)
}

func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context {
	if data == nil {
		return context.WithValue(ctx, ctxKeySession, &AuthData{User: user})
	}
	return context.WithValue(
		ctx,
		ctxKeySession,
		&AuthData{
			User:    user,
			Token:   data.Token,
			Now:     data.Now,
			Issued:  data.Issued,
			Expires: data.Expires,
		})
}

func (srv *webServer) Run() error { return srv.httpServer.start() }
func (srv *webServer) Stop()      { srv.httpServer.stop() }

// Server timeout values
const shutdownTimeout = 5 * time.Second







|









|






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







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
		Secure:   srv.secureCookie,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	}
	if srv.persistentCookie && d > 0 {
		cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC()
	}
	srv.log.Debug("SetToken", "token", token)
	if v := cookie.String(); v != "" {
		w.Header().Add("Set-Cookie", v)
		w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`)
		w.Header().Add("Vary", "Cookie")
	}
}

// ClearToken invalidates the session cookie by sending an empty one.
func (srv *webServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context {
	if authData := user.GetAuthData(ctx); authData == nil {
		// No authentication data stored in session, nothing to do.
		return ctx
	}
	if w != nil {
		srv.SetToken(w, nil, 0)
	}
	return user.UpdateContext(ctx, nil, nil)
















}

func (srv *webServer) Run() error { return srv.httpServer.start() }
func (srv *webServer) Stop()      { srv.httpServer.stop() }

// Server timeout values
const shutdownTimeout = 5 * time.Second
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// 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) }()
	return nil
}

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

	srv.Shutdown(ctx)
}







|








|

168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// 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) }()
	return nil
}

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

	_ = srv.Shutdown(ctx)
}
Changes to internal/web/server/router.go.
10
11
12
13
14
15
16
17
18
19
20
21
22
23



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


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


61
62
63
64
65
66
67
68


69
70
71
72
73
74
75
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package server

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




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

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


	profiling      bool
}

// initializeRouter creates a new, empty router with the given root handler.
func (rt *httpRouter) initializeRouter(rd routerData) {
	rt.log = rd.log
	rt.urlPrefix = rd.urlPrefix
	rt.auth = rd.auth


	rt.minKey = 255
	rt.maxKey = 0
	rt.reURL = regexp.MustCompile("^$")
	rt.mux = http.NewServeMux()
	rt.maxReqSize = rd.maxRequestSize

	if rd.profiling {







|






>
>
>

|

















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



|



>
>








>
>







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

package server

import (
	"log/slog"
	"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/auth/user"
)

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           *slog.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            *slog.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 {
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192













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


212


213
214
215
216
217
218
219
220
221
222

// Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics.
func (rt *httpRouter) Handle(pattern string, handler http.Handler) {
	rt.mux.Handle(pattern, handler)
}

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

	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")
			}
			return
		}
		r.URL.Path = r.URL.Path[prefixLen-1:]
	}
	r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize)
	match := rt.reURL.FindStringSubmatch(r.URL.Path)
	if len(match) != 3 {
		rt.mux.ServeHTTP(w, rt.addUserContext(r))
		if withDebug {
			rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("match other")
		}
		return
	}
	if withDebug {
		rt.log.Debug().Str("key", match[1]).Str("zid", match[2]).Msg("path match")
	}

	key := match[1][0]
	var mh *methodHandler
	if match[2] == "" {
		mh = rt.listTable[key]
	} else {
		mh = rt.zettelTable[key]
	}
	method, ok := mapMethod[r.Method]
	if ok && mh != nil {
		if handler := mh[method]; handler != nil {
			r.URL.Path = "/" + match[2]
			handler.ServeHTTP(w, rt.addUserContext(r))
			if withDebug {
				rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP")
			}
			return
		}
	}
	http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	if withDebug {
		rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match")
	}
}

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













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


		return r
	}
	return r.WithContext(updateContext(ctx, user, &tokenData))
}

func getSessionToken(r *http.Request) []byte {
	cookie, err := r.Cookie(sessionName)
	if err != nil {
		return nil
	}







<
<
<
<
<
<
<



<
<
<








<
<
<


<
<
<













<
<
<




<
<
<







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



|




|




|


<
|

>
>
|
>
>


|







136
137
138
139
140
141
142







143
144
145



146
147
148
149
150
151
152
153



154
155



156
157
158
159
160
161
162
163
164
165
166
167
168



169
170
171
172



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208

209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225

// Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics.
func (rt *httpRouter) Handle(pattern string, handler http.Handler) {
	rt.mux.Handle(pattern, handler)
}

func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {







	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)



			return
		}
		r.URL.Path = r.URL.Path[prefixLen-1:]
	}
	r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize)
	match := rt.reURL.FindStringSubmatch(r.URL.Path)
	if len(match) != 3 {
		rt.mux.ServeHTTP(w, rt.addUserContext(r))



		return
	}




	key := match[1][0]
	var mh *methodHandler
	if match[2] == "" {
		mh = rt.listTable[key]
	} else {
		mh = rt.zettelTable[key]
	}
	method, ok := mapMethod[r.Method]
	if ok && mh != nil {
		if handler := mh[method]; handler != nil {
			r.URL.Path = "/" + match[2]
			handler.ServeHTTP(w, rt.addUserContext(r))



			return
		}
	}
	http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)



}

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 u, err := rt.ur.GetUser(ctx, rt.loopbackZid, rt.loopbackIdent); err == nil {
				if u != nil {
					return r.WithContext(user.UpdateContext(ctx, u, nil))
				}
				rt.log.Error("No match to loopback-zid", "loopback-ident", rt.loopbackIdent)
			}
		}
	}

	k := auth.KindAPI
	t := getHeaderToken(r)
	if len(t) == 0 {
		rt.log.Debug("no jwt token found") // IP already logged: ServeHTTP
		k = auth.KindwebUI
		t = getSessionToken(r)
	}
	if len(t) == 0 {
		rt.log.Debug("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("invalid auth token", "err", err, "remote", ip.GetRemoteAddr(r))
		return r
	}

	u, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident)
	if err != nil {
		rt.log.Info("auth user not found",
			"zid", tokenData.Zid,
			"ident", tokenData.Ident,
			"err", err,
			"remote", ip.GetRemoteAddr(r))
		return r
	}
	return r.WithContext(user.UpdateContext(ctx, u, &tokenData))
}

func getSessionToken(r *http.Request) []byte {
	cookie, err := r.Cookie(sessionName)
	if err != nil {
		return nil
	}
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
	const prefix = "Bearer "
	// RFC 2617, subsection 1.2 defines the scheme token as case-insensitive.
	if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
		return nil
	}
	return []byte(auth[len(prefix):])
}

type traceResponseWriter struct {
	original   http.ResponseWriter
	statusCode int
}

func (w *traceResponseWriter) Header() http.Header         { return w.original.Header() }
func (w *traceResponseWriter) Write(p []byte) (int, error) { return w.original.Write(p) }
func (w *traceResponseWriter) WriteHeader(statusCode int) {
	w.statusCode = statusCode
	w.original.WriteHeader(statusCode)
}
func (w *traceResponseWriter) WriteString(s string) (int, error) {
	return io.WriteString(w.original, s)
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
241
242
243
244
245
246
247















	const prefix = "Bearer "
	// RFC 2617, subsection 1.2 defines the scheme token as case-insensitive.
	if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
		return nil
	}
	return []byte(auth[len(prefix):])
}















Changes to internal/web/server/server.go.
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
	// SetToken sends the token to the client.
	SetToken(w http.ResponseWriter, token []byte, d time.Duration)

	// ClearToken invalidates the session cookie by sending an empty one.
	ClearToken(ctx context.Context, w http.ResponseWriter) context.Context
}

// AuthData stores all relevant authentication data for a context.
type AuthData struct {
	User    *meta.Meta
	Token   []byte
	Now     time.Time
	Issued  time.Time
	Expires time.Time
}

// GetAuthData returns the full authentication data from the context.
func GetAuthData(ctx context.Context) *AuthData {
	if ctx != nil {
		data, ok := ctx.Value(ctxKeySession).(*AuthData)
		if ok {
			return data
		}
	}
	return nil
}

// GetUser returns the metadata of the current user, or nil if there is no one.
func GetUser(ctx context.Context) *meta.Meta {
	if data := GetAuthData(ctx); data != nil {
		return data.User
	}
	return nil
}

// ctxKeyTypeSession is just an additional type to make context value retrieval unambiguous.
type ctxKeyTypeSession struct{}

// ctxKeySession is the key value to retrieve Authdata
var ctxKeySession ctxKeyTypeSession

// AuthBuilder is a Builder that also allows to execute authentication functions.
type AuthBuilder interface {
	Auth
	Builder
}

// Server is the main web server for accessing Zettelstore via HTTP.







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







62
63
64
65
66
67
68


































69
70
71
72
73
74
75
	// SetToken sends the token to the client.
	SetToken(w http.ResponseWriter, token []byte, d time.Duration)

	// ClearToken invalidates the session cookie by sending an empty one.
	ClearToken(ctx context.Context, w http.ResponseWriter) context.Context
}



































// AuthBuilder is a Builder that also allows to execute authentication functions.
type AuthBuilder interface {
	Auth
	Builder
}

// Server is the main web server for accessing Zettelstore via HTTP.
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
		}

	}
}

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

	testdata := []struct {
		user string
		exp  int







|
|
|
|







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      = 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
	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()
	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()
	bodyEmoji, err := io.ReadAll(respEmoji.Body)
	if err != nil {
		t.Error(err)
		return
	}
	if !slices.Equal(bodyRedirect, bodyEmoji) {
		t.Error("Wrong redirect")







|











|







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

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)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()

		testID = tc.Example*100 + 2
		secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &secondAst)
		gotSecond := buf.String()

		// if gotFirst != gotSecond {
		// 	st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond)
		// }

		testID = tc.Example*100 + 3
		thirdAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &thirdAst)
		gotThird := buf.String()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
		}
	})
}

func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, "abc``<br>``{=\"html\"}def"},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		sb.Reset()
		zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		if got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
	}
}







|











|





|









|




















|






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)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()

		testID = tc.Example*100 + 2
		secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, &secondAst)
		gotSecond := buf.String()

		// if gotFirst != gotSecond {
		// 	st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond)
		// }

		testID = tc.Example*100 + 3
		thirdAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, &thirdAst)
		gotThird := buf.String()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
		}
	})
}

func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, "abc``<br>``{=\"html\"}def"},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		sb.Reset()
		_, _ = zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		if got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
	}
}
Changes to tests/naughtystrings_test.go.
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()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		if text := scanner.Text(); text != "" && text[0] != '#' {
			result = append(result, text)
		}
	}
	return result, scanner.Err()







|







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

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)




	return string(src), err
}

func checkFileContent(t *testing.T, filename, gotContent string) {
	t.Helper()
	wantContent, err := resultFile(filename)
	if err != nil {







<

>
>
>
>







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
	}

	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
}

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







|







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)
		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
		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()
	zipWriter := zip.NewWriter(zipFile)
	defer zipWriter.Close()

	for _, entry := range entries {
		if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
			return err
		}
	}
	return nil







|

|







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 func() { _ = zipFile.Close() }()
	zipWriter := zip.NewWriter(zipFile)
	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
	if err != nil {
		return err
	}
	manualFile, err := os.Open(filepath.Join(path, name))
	if err != nil {
		return err
	}
	defer manualFile.Close()

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

	data, err := io.ReadAll(manualFile)







|







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

//--- release







<
|







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
	}

	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
}

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()
	zw := zip.NewWriter(zipFile)
	defer zw.Close()
	err = addFileToZip(zw, zsName, fileName)
	if err != nil {
		return err
	}
	err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt")
	if err != nil {
		return err
	}
	err = 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()
	stat, err := zsFile.Stat()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(stat)
	if err != nil {
		return err







|

|
|
<


|
<


|
<







|







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 func() { _ = zipFile.Close() }()
	zw := zip.NewWriter(zipFile)
	defer func() { _ = zw.Close() }()
	if err = addFileToZip(zw, zsName, fileName); err != nil {

		return err
	}
	if err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt"); err != nil {

		return err
	}
	return addFileToZip(zw, "docs/readmezip.txt", "README.txt")

}

func addFileToZip(zipFile *zip.Writer, filepath, filename string) error {
	zsFile, err := os.Open(filepath)
	if err != nil {
		return err
	}
	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

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

	return err
}

func addressInUse(address string) bool {
	conn, err := net.Dial("tcp", address)
	if err != nil {
		return false
	}
	conn.Close()
	return true
}







|
|
>
|
>








|


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
	}
	_, 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()
	return true
}
Changes to tools/tools.go.
93
94
95
96
97
98
99



100
101
102
103
104
105
106
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	if err := checkRevive(); err != nil {
		return err



	}
	if forRelease {
		if err := checkGoVulncheck(); err != nil {
			return err
		}
	}
	return checkFossilExtra()







>
>
>







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











	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {







>
>
>
>
>
>
>
>
>
>
>







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
12
13
14
15
16
<title>Change Log</title>

<a id="0_22"></a>
<h2>Changes for Version 0.22.0 (pending)</h2>






























<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.
     Different link types are now specified by reference node.
     (breaking: api)
  *  Sz encodings of lists, descriptions, tables, table cells, and block BLOBs
     have now an additional attributes entry, directly after the initial
     symbol.




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



|
|







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
<title>Change Log</title>

<a id="0_22"></a>
<h2>Changes for Version 0.22.0 (pending)</h2>
  *  Sx builtin <code>(bind-lookup ...)</code> is replaced with
     <code>(resolve-symbol ...)</code>. If you maintain your own Sx code to
     customize Zettelstore behaviour, you must update your code; otherwise it
     will break. If your code use <code>(ROLE-DEFAULT-action ...)</code>
     , infinite recursion might occur. This is now handled with the new startup
     configuration <code>sx-max-nesting</code>. In such cases, you should omit
     the call to <code>ROLE-DEFAULT-action</code>. For debugging purposes, use
     the <code>web:trace</code> logging level, which logs all Sx computations.
     (breaking: webui)
  *  Sx templates are changed: base.sxn, info.sxn, zettel.sxn. If you are using
     a (self-) modified version, you should update your modifications.
     (breaking: webui)
  *  Remove zettel for the Sx prelude. Fortunately, it was never documented,
     so it was likely unused. The prelude is now a constant string whithin Sx's
     code base.
     (breaking: webui)
  *  If authentication is enabled, Zettelstore can now be accessed from the
     loopback device without logging in or obtaining an access token.
     (major: api, webui)
  *  The new startup configuration key <code>sx-max-nesting</code> allows
     setting a limit on the nesting depth of Sx computations. This is primarily
     useful to prevent unbounded recursion due to programming errors.
     Previously, such issues would crash the Zettelstore.
     (minor: webui)
  *  At logging level <code>trace</code>, the web user interface now logs all
     Sx computations, mainly those used for rendering HTML templates.
     (minor: 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>
  *  Change zettel identifier for Zettelstore Log, Zettelstore Memory, and
     Zettelstore Sx Engine. See manual for updated identifier values.
     (breaking)
  *  Sz encodings of links were simplified into one <code>LINK</code> symbol.
     Different link types are now specified by reference node.
     (breaking: api)
  *  Sz encodings of lists, descriptions, tables, table cells, and block BLOBs
     have now an additional attributes entry, directly after the initial
     symbol.