Zettelstore

Check-in Differences
Login

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

Difference From v0.10.0 To v0.11.2

2023-04-16
16:50
Version 0.11.2 ... (Leaf check-in: 61f8578997 user: stern tags: release, release-0.11, v0.11.2)
16:42
Fix: render footer zettel on all WebUI pages correctly ... (check-in: ba4ca6730f user: stern tags: trunk)
2023-03-28
16:28
Version 0.11.1 ... (check-in: fede87bf67 user: stern tags: trunk, release, v0.11.1)
2023-01-30
12:40
Version 0.10.1 ... (Leaf check-in: 70b0cdc454 user: stern tags: release, release-0.10, v0.10.1)
2023-01-25
21:10
Increase version to 0.11.0-dev to begin next development cycle ... (check-in: 65f364a414 user: t73fde tags: trunk)
2023-01-24
21:47
Version 0.10.0 ... (check-in: b6301cf091 user: stern tags: trunk, release, v0.10.0)
20:03
Update to Zettelstore client v0.10.0 ... (check-in: 65115c3a16 user: stern tags: trunk)

Changes to LICENSE.txt.

1

2
3
4
5
6
7
8

1
2
3
4
5
6
7
8
-
+







Copyright (c) 2020-2023 Detlef Stern
Copyright (c) 2020-present Detlef Stern

                          Licensed under the EUPL

Zettelstore is licensed under the European Union Public License, version 1.2 or
later (EUPL v. 1.2). The license is available in the official languages of the
EU. The English version is included here. Please see
https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official

Changes to Makefile.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+








## Copyright (c) 2020-2022 Detlef Stern
## Copyright (c) 2020-present Detlef Stern
##
## This file is part of Zettelstore.
##
## Zettelstore is licensed under the latest version of the EUPL (European Union
## Public License). Please see file LICENSE.txt for your rights and obligations
## under this license.

Changes to VERSION.

1


1
-
+
0.10.0
0.11.2

Changes to ast/ast.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to ast/block.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to ast/inline.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to ast/ref.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to ast/ref_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to ast/walk.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to ast/walk_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to auth/auth.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to auth/cred/cred.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to auth/impl/impl.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to auth/policy/anon.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to auth/policy/box.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to auth/policy/default.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to auth/policy/owner.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to auth/policy/policy.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to auth/policy/policy_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to auth/policy/readonly.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/box.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
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

-
+
















-
-







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

// Package box provides a generic interface to zettel boxes.
package box

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"strconv"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
89
90
91
92
93
94
95















96
97
98



99
100

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







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



+
+
+

-
+







type ManagedBoxStats struct {
	// ReadOnly indicates that the content of a box cannot change.
	ReadOnly bool

	// Zettel is the number of zettel managed by the box.
	Zettel int
}

// StartState enumerates the possible states of starting and stopping a box.
//
// StartStateStopped -> StartStateStarting -> StartStateStarted -> StateStateStopping -> StartStateStopped.
//
// Other transitions are also possible.
type StartState uint8

// Constant values of StartState
const (
	StartStateStopped StartState = iota
	StartStateStarting
	StartStateStarted
	StartStateStopping
)

// StartStopper performs simple lifecycle management.
type StartStopper interface {
	// State the current status of the box.
	State() StartState

	// Start the box. Now all other functions of the box are allowed.
	// Starting an already started box is not allowed.
	// Starting a box, which is not in state StartStateStopped is not allowed.
	Start(ctx context.Context) error

	// Stop the started box. Now only the Start() function is allowed.
	Stop(ctx context.Context)
}

// Refresher allow to refresh their internal data.
179
180
181
182
183
184
185

186
187
188
189
190
191
192

193
194
195
196
197
198
199
195
196
197
198
199
200
201
202
203
204
205
206
207
208

209
210
211
212
213
214
215
216







+






-
+








// UpdateReason gives an indication, why the ObserverFunc was called.
type UpdateReason uint8

// Values for Reason
const (
	_        UpdateReason = iota
	OnReady               // Box is started and fully operational
	OnReload              // Box was reloaded
	OnZettel              // Something with a zettel happened
)

// UpdateInfo contains all the data about a changed zettel.
type UpdateInfo struct {
	Box    Box
	Box    BaseBox
	Reason UpdateReason
	Zid    id.Zid
}

// UpdateFunc is a function to be called when a change is detected.
type UpdateFunc func(UpdateInfo)

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
310
311
312
313
314
315
316
































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

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

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

// GetQueryBool is a helper function to extract bool values from a box URI.
func GetQueryBool(u *url.URL, key string) bool {
	_, ok := u.Query()[key]
	return ok
}

// GetQueryInt is a helper function to extract int values of a specified range from a box URI.
func GetQueryInt(u *url.URL, key string, min, def, max int) int {
	sVal := u.Query().Get(key)
	if sVal == "" {
		return def
	}
	iVal, err := strconv.Atoi(sVal)
	if err != nil {
		return def
	}
	if iVal < min {
		return min
	}
	if iVal > max {
		return max
	}
	return iVal
}

Changes to box/compbox/compbox.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/compbox/config.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/compbox/keys.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/compbox/log.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/compbox/manager.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/compbox/parser.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/compbox/version.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/constbox/base.css.

140
141
142
143
144
145
146

147
148
149
150
151
152
153
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154







+







  input.zs-secondary { float:left }
  input.zs-upload {
    padding-left: 1em;
    padding-right: 1em;
  }
  a:not([class]) { text-decoration-skip-ink: auto }
  a.broken { text-decoration: line-through }
  a.external::after { content: "➚"; display: inline-block }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }

Changes to box/constbox/constbox.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/constbox/context.mustache.


1
2
3
4
5
6
7
8
9
10
11

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











+
<article>
<header>
<h1>{{Title}}</h1>
<div class="zs-meta">
<a href="{{{InfoURL}}}">Info</a>
&#183; <a href="?dir=backward">Backward</a>
&#183; <a href="?dir=both">Both</a>
&#183; <a href="?dir=forward">Forward</a>
&#183; Cost:{{#Costs}}&#x2000;<a href="{{{URL}}}">{{{Text}}}</a>{{/Costs}}
</div>
</header>
{{{Content}}}
</article>

Changes to box/constbox/delete.mustache.

36
37
38
39
40
41
42
43
36
37
38
39
40
41
42








-
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
<form method="POST">
<input class="zs-primary" type="submit" value="Delete">
</form>
</article>
{{end}}

Changes to box/constbox/license.txt.

1

2
3
4
5
6
7
8

1
2
3
4
5
6
7
8
-
+







Copyright (c) 2020-2023 Detlef Stern
Copyright (c) 2020-present Detlef Stern

                          Licensed under the EUPL

Zettelstore is licensed under the European Union Public License, version 1.2 or
later (EUPL v. 1.2). The license is available in the official languages of the
EU. The English version is included here. Please see
https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official

Changes to box/constbox/listzettel.mustache.


1
2
3
4
5
6
7
8

9
10
11
12


1
2
3
4
5
6
7
8

9
10
11
12

13
14
+







-
+



-
+
+
<article>
<header>
<h1>{{Title}}</h1>
</header>
<form action="{{{SearchURL}}}">
<input class="zs-input" type="text" placeholder="Search.." name="{{QueryKeyQuery}}" value="{{QueryValue}}">
</form>
{{{Content}}}
<form action="{{{CreateURL}}}">
{{#CanCreate}}<form action="{{{CreateURL}}}">
<input type="hidden" name="{{QueryKeyQuery}}" value="{{QueryValue}}">
<input type="hidden" name="{{QueryKeySeed}}" value="{{Seed}}">
<input class="zs-primary" type="submit" value="Save As Zettel">
</form>
</form>{{/CanCreate}}
</article>

Changes to box/constbox/zettel.mustache.

1
2
3

4
5
6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
1
2

3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22


-
+











-
+







<article>
<header>
<h1>{{{HTMLTitle}}}</h1>
<h1>{{Heading}}</h1>
<div class="zs-meta">
{{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> &#183;{{/CanWrite}}
{{Zid}} &#183;
<a href="{{{InfoURL}}}">Info</a> &#183;
(<a href="{{{RoleURL}}}">{{RoleText}}</a>)
{{#Tags.Has}}&#183; {{#Tags.Links}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags.Links}}{{/Tags.Has}}
{{#CanCopy}}&#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanVersion}}&#183; <a href="{{{VersionURL}}}">Version</a>{{/CanVersion}}
{{#CanFolge}}&#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#PredecessorRefs}}<br>Predecessor: {{{PredecessorRefs}}}{{/PredecessorRefs}}
{{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}}
{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}}
{{#HasExtURL}}<br>URL: {{{ExtURL}}}{{/HasExtURL}}
{{#Author}}<br>By {{Author}}{{/Author}}
</div>
</header>
{{{Content}}}
</article>
{{#NeedBottomNav}}<nav>{{/NeedBottomNav}}
{{#FolgeLinks.Has}}

Changes to box/dirbox/dirbox.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
124
125
126
127
128
129
130


















131
132
133
134
135
136
137
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







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







	fCmds      []chan fileCmd
	mxCmds     sync.RWMutex
}

func (dp *dirBox) Location() string {
	return dp.location
}

func (dp *dirBox) State() box.StartState {
	if ds := dp.dirSrv; ds != nil {
		switch ds.State() {
		case notify.DsCreated:
			return box.StartStateStopped
		case notify.DsStarting:
			return box.StartStateStarting
		case notify.DsWorking:
			return box.StartStateStarted
		case notify.DsMissing:
			return box.StartStateStarted
		case notify.DsStopping:
			return box.StartStateStopping
		}
	}
	return box.StartStateStopped
}

func (dp *dirBox) Start(context.Context) error {
	dp.mxCmds.Lock()
	defer dp.mxCmds.Unlock()
	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
	for i := uint32(0); i < dp.fSrvs; i++ {
		cc := make(chan fileCmd)
149
150
151
152
153
154
155

156
157
158
159
160
161
162
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181







+







	}
	if err != nil {
		dp.log.Fatal().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
}

Changes to box/dirbox/dirbox_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/dirbox/service.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/filebox/filebox.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/filebox/zipbox.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
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







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








-
+











+








func (zb *zipBox) Location() string {
	if strings.HasPrefix(zb.name, "/") {
		return "file://" + zb.name
	}
	return "file:" + zb.name
}

func (zb *zipBox) State() box.StartState {
	if ds := zb.dirSrv; ds != nil {
		switch ds.State() {
		case notify.DsCreated:
			return box.StartStateStopped
		case notify.DsStarting:
			return box.StartStateStarting
		case notify.DsWorking:
			return box.StartStateStarted
		case notify.DsMissing:
			return box.StartStateStarted
		case notify.DsStopping:
			return box.StartStateStopping
		}
	}
	return box.StartStateStopped
}

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.log, zipNotifier, zb.notify)
	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 (*zipBox) CanCreateZettel(context.Context) bool { return false }

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

Changes to box/helper.go.

1
2

3
4
5
6
7
8
9
10
11
12
13


14
15
16
17
18
19
20
1

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

-
+











+
+







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

package box

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

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

// GetNewZid calculates a new and unused zettel identifier, based on the current date and time.
func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) {
30
31
32
33
34
35
36

























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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
		}
		// TODO: do not wait here unconditionally.
		time.Sleep(100 * time.Millisecond)
		withSeconds = true
	}
	return id.Invalid, ErrConflict
}

// GetQueryBool is a helper function to extract bool values from a box URI.
func GetQueryBool(u *url.URL, key string) bool {
	_, ok := u.Query()[key]
	return ok
}

// GetQueryInt is a helper function to extract int values of a specified range from a box URI.
func GetQueryInt(u *url.URL, key string, min, def, max int) int {
	sVal := u.Query().Get(key)
	if sVal == "" {
		return def
	}
	iVal, err := strconv.Atoi(sVal)
	if err != nil {
		return def
	}
	if iVal < min {
		return min
	}
	if iVal > max {
		return max
	}
	return iVal
}

Changes to box/manager/anteroom.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/manager/anteroom_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/manager/box.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
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

-
+











-


+















-
+


-
+

-
+

-
+






-
+





-
-
+
-


+
+






-
-
+
-


+
+














-
-
+
-


+
+













-
-
+
-


+
+


















-
-
+
-


+
+













-
-
+
-



+
+

















-
-
+
-


+
+







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

package manager

import (
	"bytes"
	"context"
	"errors"
	"strings"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
)

// Conatains all box.Box related functions

// Location returns some information where the box is located.
func (mgr *Manager) Location() string {
	if len(mgr.boxes) <= 2 {
		return "NONE"
	}
	var buf bytes.Buffer
	var sb strings.Builder
	for i := 0; i < len(mgr.boxes)-2; i++ {
		if i > 0 {
			buf.WriteString(", ")
			sb.WriteString(", ")
		}
		buf.WriteString(mgr.boxes[i].Location())
		sb.WriteString(mgr.boxes[i].Location())
	}
	return buf.String()
	return sb.String()
}

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

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

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

// GetAllZettel retrieves a specific zettel from all managed boxes.
func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return nil, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	var result []domain.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
}

// GetMeta retrieves just the meta data of a specific zettel.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return nil, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.doGetMeta(ctx, zid)
}

func (mgr *Manager) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	for i, p := range mgr.boxes {
		if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, m, i+1)
			}
			return m, err
		}
	}
	return nil, box.ErrNotFound
}

// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes.
func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetAllMeta")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return nil, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	var result []*meta.Meta
	for i, p := range mgr.boxes {
		if m, err := p.GetMeta(ctx, zid); err == nil {
			mgr.Enrich(ctx, m, i+1)
			result = append(result, m)
		}
	}
	return result, nil
}

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZids")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return nil, box.ErrStopped
	}
	result := id.Set{}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Zid(zid) }, func(id.Zid) bool { return true })
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

type metaMap map[id.Zid]*meta.Meta

// 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, q *query.Query) ([]*meta.Meta, error) {
	if msg := mgr.mgrLog.Debug(); msg.Enabled() {
		msg.Str("query", q.String()).Msg("SelectMeta")
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return nil, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()

	compSearch := q.RetrieveAndCompile(mgr)
	selected := metaMap{}
	for _, term := range compSearch.Terms {
		rejected := id.Set{}
		handleMeta := func(m *meta.Meta) {
			zid := m.Zid
195
196
197
198
199
200
201
202

203
204
205
206
207
208
209

210
211
212
213
214
215
216

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233

234
235
236


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

250
251
252


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

269
270
271


272
273
274
275
276
277
278
279
280
281
282
283
284

285
286
287


288
289
290
291
292
293
294
295
296
297
298
195
196
197
198
199
200
201

202
203
204
205
206
207
208

209
210
211
212
213
214


215

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


230

231
232
233
234
235
236
237
238
239
240
241
242
243
244
245


246

247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264


265

266
267
268
269
270
271
272
273
274
275
276
277
278
279
280


281

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296







-
+






-
+





-
-
+
-














-
-
+
-


+
+











-
-
+
-


+
+














-
-
+
-


+
+











-
-
+
-


+
+











			}
		}
	}
	result := make([]*meta.Meta, 0, len(selected))
	for _, m := range selected {
		result = append(result, m)
	}
	return q.Sort(result), nil
	return q.AfterSearch(result), nil
}

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

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

// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if !p.AllowRenameZettel(ctx, zid) {
			return false
		}
	}
	return true
}

// RenameZettel changes the current zid to a new zid.
func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for i, p := range mgr.boxes {
		err := p.RenameZettel(ctx, curZid, newZid)
		if err != nil && !errors.Is(err, box.ErrNotFound) {
			for j := 0; j < i; j++ {
				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}
	return nil
}

// CanDeleteZettel returns true, if box could possibly delete the given zettel.
func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if mgr.State() != box.StartStateStarted {
	if !mgr.started {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

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

Changes to box/manager/collect.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/manager/enrich.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/manager/indexer.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
81
82
83
84
85
86
87
88

89
90
91
92
93
94
95
96
97
98
99
81
82
83
84
85
86
87

88
89
90
91

92
93
94
95
96
97
98







-
+



-







		}
	}()

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := box.NoEnrichContext(context.Background())
	for {
		// Sleep first, so the indexer will wait for boxes to initialize.
		mgr.idxWorkService(ctx)
		if !mgr.idxSleepService(timer, timerDuration) {
			return
		}
		mgr.idxWorkService(ctx)
	}
}

func (mgr *Manager) idxWorkService(ctx context.Context) {
	var roomNum uint64
	var start time.Time
	for {

Changes to box/manager/manager.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
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







-
-
+
+
+




















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








// GetSchemes returns all registered scheme, ordered by scheme string.
func GetSchemes() []string { return maps.Keys(registry) }

// Manager is a coordinating box.
type Manager struct {
	mgrLog       *logger.Logger
	mgrMx        sync.RWMutex
	started      bool
	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 strfun.Set // Set of property key names

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

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

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

func (mgr *Manager) State() box.StartState {
	mgr.stateMx.RLock()
	state := mgr.state
	mgr.stateMx.RUnlock()
	return state
}

// New creates a new managing box.
func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) {
	descrs := meta.GetSortedKeyDescriptions()
	propertyKeys := make(strfun.Set, len(descrs))
	for _, kd := range descrs {
		if kd.IsProperty() {
191
192
193
194
195
196
197

198
199
200
201

202


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

218
219
220
221
222
223
224
225
226







+




+
-
+
+








				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
				}

				mgr.idxEnqueue(reason, zid)
				if ci.Box == nil {
					ci.Box = mgr
				}
				if mgr.State() == box.StartStateStarted {
				mgr.notifyObserver(&ci)
					mgr.notifyObserver(&ci)
				}
			}
		case <-mgr.done:
			return
		}
	}
}

224
225
226
227
228
229
230


231
232
233
234
235

236
237
238
239
240
241
242
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262







+
+





+







		reason: reason,
	}
	return false
}

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







-
-
+
+


+









+





-
+





+
+
+
+
+
+
+
+

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





-
+


+






-
+





-
-
+
-


+
+







	}
}

// Start the box. Now all other functions of the box are allowed.
// Starting an already started box is not allowed.
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mgrMx.Lock()
	if mgr.started {
		mgr.mgrMx.Unlock()
	defer mgr.mgrMx.Unlock()
	if mgr.State() != box.StartStateStopped {
		return box.ErrStarted
	}
	mgr.setState(box.StartStateStarting)
	for i := len(mgr.boxes) - 1; i >= 0; i-- {
		ssi, ok := mgr.boxes[i].(box.StartStopper)
		if !ok {
			continue
		}
		err := ssi.Start(ctx)
		if err == nil {
			continue
		}
		mgr.setState(box.StartStateStopping)
		for j := i + 1; j < len(mgr.boxes); j++ {
			if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 {
				ssj.Stop(ctx)
			}
		}
		mgr.mgrMx.Unlock()
		mgr.setState(box.StartStateStopped)
		return err
	}
	mgr.idxAr.Reset() // Ensure an initial index run
	mgr.done = make(chan struct{})
	go mgr.notifier()

	for !mgr.allBoxesStarted() {
		mgr.mgrLog.Trace().Msg("Wait for boxes to start")
		time.Sleep(time.Second)
	}
	mgr.setState(box.StartStateStarted)
	mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady})

	go mgr.idxIndexer()
	return nil

	mgr.started = true
	mgr.mgrMx.Unlock()
	return nil
}
}

func (mgr *Manager) allBoxesStarted() bool {
	for _, bx := range mgr.boxes {
		if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted {
			return false
		}
	}
	return true
}

// Stop the started box. Now only the Start() function is allowed.
func (mgr *Manager) Stop(ctx context.Context) {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if !mgr.started {
	if mgr.State() != box.StartStateStarted {
		return
	}
	mgr.setState(box.StartStateStopping)
	close(mgr.done)
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			ss.Stop(ctx)
		}
	}
	mgr.started = false
	mgr.setState(box.StartStateStopped)
}

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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/membox/membox.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
50
51
52
53
54
55
56
57

58
59
60
61
62
63









64
65
66
67
68
69
70
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







-
+






+
+
+
+
+
+
+
+
+







	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]domain.Zettel
	curBytes  int
}

func (mb *memBox) notifyChanged(zid id.Zid) {
	if chci := mb.cdata.Notify; chci != nil {
		chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid}
		chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid}
	}
}

func (mb *memBox) Location() string {
	return mb.u.String()
}

func (mb *memBox) State() box.StartState {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	if mb.zettel == nil {
		return box.StartStateStopped
	}
	return box.StartStateStarted
}

func (mb *memBox) Start(context.Context) error {
	mb.mx.Lock()
	mb.zettel = make(map[id.Zid]domain.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")

Changes to box/notify/directory.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
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
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







-
+








-
+


-
-
-
-
-
+
+
+
+
+




+





-
+







-
+

+



-
+


+
+
+
+
+
+
+
+




-
+













-
+







	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
)

type entrySet map[id.Zid]*DirEntry

// directoryState signal the internal state of the service.
// DirServiceState signal the internal state of the service.
//
// The following state transitions are possible:
// --newDirService--> dsCreated
// dsCreated --Start--> dsStarting
// dsStarting --last list notification--> dsWorking
// dsWorking --directory missing--> dsMissing
// dsMissing --last list notification--> dsWorking
// --Stop--> dsStopping
type directoryState uint8
type DirServiceState uint8

const (
	dsCreated  directoryState = iota
	dsStarting                // Reading inital scan
	dsWorking                 // Initial scan complete, fully operational
	dsMissing                 // Directory is missing
	dsStopping                // Service is shut down
	DsCreated  DirServiceState = iota
	DsStarting                 // Reading inital scan
	DsWorking                  // Initial scan complete, fully operational
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    chan<- box.UpdateInfo
	mx       sync.RWMutex // protects status, entries
	state    directoryState
	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(log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    chci,
		state:    dsCreated,
		state:    DsCreated,
	}
}

// State the current service state.
func (ds *DirService) State() DirServiceState {
	ds.mx.RLock()
	state := ds.state
	ds.mx.RUnlock()
	return state
}

// Start the directory service.
func (ds *DirService) Start() {
	ds.mx.Lock()
	ds.state = dsStarting
	ds.state = DsStarting
	ds.mx.Unlock()
	var newEntries entrySet
	go ds.updateEvents(newEntries)
}

// Refresh the directory entries.
func (ds *DirService) Refresh() {
	ds.notifier.Refresh()
}

// Stop the directory service.
func (ds *DirService) Stop() {
	ds.mx.Lock()
	ds.state = dsStopping
	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")
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
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







-
+






-
+








-
+


-
+







	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 {
	if state == DsStopping {
		return nil, false
	}

	switch ev.Op {
	case Error:
		newEntries = nil
		if state != dsMissing {
		if state != DsMissing {
			ds.log.Warn().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
			fromMissing := ds.state == DsMissing
			prevEntries := ds.entries
			ds.entries = newEntries
			ds.state = dsWorking
			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
		}
314
315
316
317
318
319
320
321

322
323
324
325
326
327
328
324
325
326
327
328
329
330

331
332
333
334
335
336
337
338







-
+







	}
}

func (ds *DirService) onDestroyDirectory() {
	ds.mx.Lock()
	entries := ds.entries
	ds.entries = nil
	ds.state = dsMissing
	ds.state = DsMissing
	ds.mx.Unlock()
	for zid := range entries {
		ds.notifyChange(zid)
	}
}

var validFileName = regexp.MustCompile(`^(\d{14})`)
591
592
593
594
595
596
597
598

599
600
601
602
603
604
605
606
607

608
609
610







-
+


	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(zid id.Zid) {
	if chci := ds.infos; chci != nil {
		ds.log.Trace().Zid(zid).Msg("notifyChange")
		chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid}
		chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid}
	}
}

Changes to box/notify/directory_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/notify/entry.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to box/notify/fsdir.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
90
91
92
93
94
95
96

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







+







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:

Changes to box/notify/helper.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/notify/notify.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/notify/simpledir.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to cmd/cmd_file.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to cmd/cmd_password.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to cmd/cmd_run.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
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
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







-
+
-






-
+
-



















-
+
-







		const assetPrefix = "/assets/"
		webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir))))
		webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir))
	}

	// Web user interface
	if !authManager.IsReadonly() {
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta))
			ucGetMeta, &ucEvaluate))
		webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename))
		webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(ucListMeta, &ucEvaluate, ucListRoles, ucListSyntax))
		webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetMeta, ucGetAllMeta))
			ucGetMeta, ucGetAllMeta, &ucEvaluate))
		webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
	}
	webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
	webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(ucListMeta, &ucEvaluate))
	webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetMeta))
	webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler())
	webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler(
		ucParseZettel, &ucEvaluate, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs))
	webSrv.AddZettelRoute('k', server.MethodGet, wui.MakeZettelContextHandler(
		ucZettelContext, &ucEvaluate))

	// API
	webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
	webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler(
		usecase.NewZettelOrder(protectedBoxManager, ucEvaluate)))
	webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(
	webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(ucGetMeta, ucUnlinkedRefs))
		ucGetMeta, ucUnlinkedRefs, &ucEvaluate))
	webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion))
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddZettelRoute('x', server.MethodGet, a.MakeZettelContextHandler(ucZettelContext))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(ucListMeta))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetMeta, ucGetZettel, ucParseZettel, ucEvaluate))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))

Changes to cmd/command.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to cmd/main.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to cmd/register.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
17
18
19
20
21
22
23

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

26
27
28
29
30
31
32
33
34







+

-









	_ "zettelstore.de/z/box/constbox"      // Allow to use global internal box.
	_ "zettelstore.de/z/box/dirbox"        // Allow to use directory box.
	_ "zettelstore.de/z/box/filebox"       // Allow to use file box.
	_ "zettelstore.de/z/box/membox"        // Allow to use in-memory box.
	_ "zettelstore.de/z/encoder/htmlenc"   // Allow to use HTML encoder.
	_ "zettelstore.de/z/encoder/mdenc"     // Allow to use markdown encoder.
	_ "zettelstore.de/z/encoder/sexprenc"  // Allow to use sexpr encoder.
	_ "zettelstore.de/z/encoder/shtmlenc"  // Allow to use SHTML encoder.
	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zjsonenc"  // Allow to use ZJSON encoder.
	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
	_ "zettelstore.de/z/kernel/impl"       // Allow kernel implementation to create itself
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

Changes to cmd/zettelstore/main.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to collect/collect.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to collect/collect_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to collect/order.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to collect/split.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to config/config.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
18
19
20
21
22
23
24

25
26
27
28
29
30
31







-







)

// Key values that are supported by Config.Get
const (
	KeyFooterZettel = "footer-zettel"
	KeyHomeZettel   = "home-zettel"
	// api.KeyLang
	KeyMarkerExternal = "marker-external"
)

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

	// Get returns the value of the given key. It searches first in the given metadata,

Changes to docs/development/20210916193200.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13

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

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





-
+







+









id: 20210916193200
title: Required Software
role: zettel
syntax: zmk
created: 20210916193200
modified: 20230109121417
modified: 20230327165135

The following software must be installed:

* A current, supported [[release of Go|https://go.dev/doc/devel/release]],
* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``,
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``,
* [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``,
* [[Fossil|https://fossil-scm.org/]],
* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only).

Make sure that the software is in your path, e.g. via:

```sh
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin
```

Changes to docs/manual/00000000000100.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13
14





-
+








id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
created: 00010101000000
default-copyright: (c) 2020-2022 by Detlef Stern <ds@zettelstore.de>
default-copyright: (c) 2020-present by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-zettel: 00001000000100
home-zettel: 00001000000000
modified: 20221205173642
site-name: Zettelstore Manual
visibility: owner

Changes to docs/manual/00001004020000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221219175720
modified: 20230317183435

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

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






54
55
56
57
58
59
60







-
-
-
-
-
-








  Default: ""en"".

  This value is used as a default value, if it is not set in an user's zettel or in a zettel.
  It is also used to specify the language for all non-zettel content, e.g. lists or search results.

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!marker-external|''marker-external'']
: Some HTML code that is displayed after a [[reference to external material|00001007040310]].

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

  Default: ""&\#10138;"", to display a ""&#10138;"" sign.
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].
  Default: ""1024"".
; [!site-name|''site-name'']
: Name of the Zettelstore instance.
  Will be used when displaying some lists.

Changes to docs/manual/00001004020200.zettel.

1
2
3
4
5
6
7

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

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







-
+











-
id: 00001004020200
title: Runtime configuration data that may be user specific or zettel specific
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20221205155521
modified: 20221212195530
modified: 20230317183403

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

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

|=Key|User:|Zettel:|Remarks
|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N|
|[[''home-zettel''|00001004020000#home-zettel]]|Y|N|
|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful
|[[''marker-external''|00001004020000#marker-external]]|Y|Y|

Changes to docs/manual/00001004051200.zettel.

1
2
3
4
5
6
7

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

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

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

23
24
25
26
27
28
29
30






-
+













+

-








id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109105434
modified: 20230316182711

Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout.
This allows Zettelstore to render files manually.
```
zettelstore file [-t FORMAT] [file-1 [file-2]]
```

; ''-t FORMAT''
: Specifies the output format.
  Supported values are:
  [[''html''|00001012920510]] (default),
  [[''md''|00001012920513]],
  [[''sexpr''|00001012920516]],
  [[''shtml''|00001012920525]],
  [[''text''|00001012920519]],
  [[''zjson''|00001012920503]] (deprecated in v0.11),
  and [[''zmk''|00001012920522]].
; ''file-1''
: Specifies the file name, where at least metadata is read.
  If ''file-2'' is not given, the zettel content is also read from here.
; ''file-2''
: File name where the zettel content is stored.

If neither ''file-1'' nor ''file-2'' are given, metadata and zettel content are read from standard input / stdin.

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







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

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

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

; [!author|''author'']
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
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







+
+
+














+
+
+










-
-







  Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used.
  In all other cases, this property is not set.

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

  It is a computed value.
  There is no need to set it via Zettelstore.
; [!query|''query'']
: Stores the [[query|00001007031140]] that was used to create the zettel.
  This is for future reference.
; [!read-only|''read-only'']
: Marks a zettel as read-only.
  The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not.
; [!role|''role'']
: Defines the role of the zettel.
  Can be used for selecting zettel.
  See [[supported zettel roles|00001006020100]].
  If not given, it is ignored.
; [!successors|''successors'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value.
  Therefore, it references all zettel that contain a new version of the content and/or metadata.
  In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons.
  In most cases, zettel referencing the current zettel should be updated to reference a successor zettel.
  The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel.
; [!summary|''summary'']
: Summarizes the content of the zettel.
  You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup.
; [!syntax|''syntax'']
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If it is not given, it defaults to ''plain''.
; [!tags|''tags'']
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must begin with the number sign character (""''#''"", U+0023).
; [!title|''title'']
: Specifies the title of the zettel.
  If not given, the value of [[''id''|#id]] will be used.

  You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup.
; [!url|''url'']
: Defines an URL / URI for this zettel that possibly references external material.
  One use case is to specify the document that the current zettel comments on.
  The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template.
; [!useless-files|''useless-files'']
: Contains the file names that are rejected to serve the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].

Changes to docs/manual/00001007702000.zettel.

1
2
3
4
5

6

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

7
8
9
10
11
12
13
14





+
-
+







id: 00001007702000
title: Search term
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20220821163727
modified: 20230327123724

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

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

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







+
+
+
+
+
+
+




















-
+

-
+







  Is true, if zettel metadata contains / does not contain the given key.
* The string ''OR'' signals that following search literals may occur alternatively in the result.

  Since search literals may be negated, it is possible to form any boolean search expression.
  Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]].

  It has no effect on the following search terms initiated with a special uppercase word.
* The string ''PICK'', followed by a non-empty sequence of spaces and a number greater zero (called ""N"").

  This will pick randomly N elements of the result list.
  A zero value of N will produce the same result as if nothing was specified.
  If specified multiple times, the lower value takes precedence.

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

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

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

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

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

  Any ordering by zettel identifier will make following order terms to be ignored.

  Example: ``ORDER id ORDER created`` will be interpreted as ``ORDER id``.
* The string ''RANDOM'' will provide a random order of the resulting list.

  Currently, only the first term specifying the order of the resulting list will be used.
  Other ordering terms will be ignored.

  A random order specification will be ignored, if there is an explicit ordering given.
  A random order specification will be ignored, if there is an explicit ordering given or if the specification contains a valid ''PICK''.

  Example: ''RANDOM ORDER published'' will be interpreted as ''ORDER published''.
  Example: ''RANDOM ORDER published'' will be interpreted as ''ORDER published'', ''PICK 3 RANDOM'' will be interpreted as ''PICK 3''.
* The string ''OFFSET'', followed by a non-empty sequence of spaces and a number greater zero (called ""N"").

  This will ignore the first N elements of the result list, based on the specified sort order.
  A zero value of N will produce the same result as if nothing was specified.
  If specified multiple times, the higher value takes precedence.

  Example: ''OFFSET 4 OFFSET 8'' will be interpreted as ''OFFSET 8''.
72
73
74
75
76
77
78
79






80
81
82
83
84
85
86

87
88
89
90
91
92







-
+
+
+
+
+
+
You may have noted that the specifications of first two items overlap somehow.
This is resolved by the following rule:
* A search term containing no [[search operator character|00001007705000]] is treated as a full-text search.
* The first search operator character found in a search term divides the term into two pieces.
  If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search.
* Otherwise, the search term is treated as a full-text search.

If a term like ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search. For example, ''ORDER 123'' will search for a zettel conatining the strings ""ORDER"" (case-insensitive) and ""123"".
If a term like ''PICK'', ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search.
For example, ''ORDER 123'' will search for a zettel conatining the strings ""ORDER"" (case-insensitive) and ""123"".

A specification ''RANDOM LIMIT n'' (with some valid value of ''n'') will be interpreted as ''PICK n''.
Similar, a specification ''PICK n'', where ''n'' is greater or equal to the length of the result list, will be interpreted as ''RANDOM''.
A specification ''LIMIT n'', where ''n'' is greater or equal to the length of the result list, will be ignored effectively.

Changes to docs/manual/00001007780000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16

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

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






-
+









+







id: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20220913134024
modified: 20230327122634

```
QueryExpression  := SearchExpression ActionExpression?
SearchExpression := SearchTerm (SPACE+ SearchTerm)*.
SearchTerm       := SearchOperator? SearchValue
                  | SearchKey SearchOperator SearchValue?
                  | SearchKey ExistOperator
                  | "OR"
                  | "RANDOM"
                  | "PICK" SPACE+ PosInt
                  | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey
                  | "OFFSET" SPACE+ PosInt
                  | "LIMIT" SPACE+ PosInt.
SearchValue      := Word.
SearchKey        := MetadataKey.
SearchOperator   := '!'
                  | ('!')? ('~' | ':' | '<' | '>').

Changes to docs/manual/00001007790000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16
1
2
3
4
5
6

7
8
9
10
11
12

13
14
15
16






-
+





-
+



id: 00001007790000
title: Useful query expressions
role: manual
tags: #example #manual #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20220917174956
modified: 20230327122547

|= Query Expression |= Meaning
| [[query:role:configuration]] | Zettel that contains some configuration data for the Zettelstore
| [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel
| [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel
| [[query:RANDOM LIMIT 40]] | 40 random zettel
| [[query:PICK 40]] | 40 random zettel
| [[query:dead?]] | Zettel with invalid / dead links
| [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel
| [[query:tags!?]] | Zettel without tags

Changes to docs/manual/00001012051400.zettel.

1
2
3
4
5
6
7

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

7
8
9
10
11
12
13
14






-
+







id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20230116183844
modified: 20230320151117

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

A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below).
An empty search expression will select all zettel.
An empty list of action, or no valid action, returns the list of all selected zettel metadata.

43
44
45
46
47
48
49
50

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

50
51
52
53
54
55
56
57







-
+







Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value.
Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]].
Without a selection, the values are the empty string.
''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans.


An implicit precondition is that the zettel must contain the given metadata key.
For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true.
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

Changes to docs/manual/00001012053500.zettel.

27
28
29
30
31
32
33
34

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

34
35
36
37
38
39
40
41







-
+







<meta name="zs-title" content="API: Retrieve evaluated metadata and content of an existing zettel in various encodings">
<meta name="zs-role" content="manual">
<meta name="zs-tags" content="#api #manual #zettelstore">
<meta name="zs-syntax" content="zmk">
<meta name="zs-back" content="00001006000000 00001012000000 00001012053600">
<meta name="zs-backward" content="00001006000000 00001012000000 00001012053600 00001012920000">
<meta name="zs-box-number" content="1">
<meta name="zs-copyright" content="(c) 2020-2022 by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-copyright" content="(c) 2020-present by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-created" content="20210726174524">
<meta name="zs-forward" content="00001006050000 00001010040100 00001012050200 00001012920000 00001012920500 00001012920503 00001012920510 00001012920519 00001012920800">
<meta name="zs-lang" content="en">
<meta name="zs-license" content="EUPL-1.2-or-later">
<meta name="zs-modified" content="20221219161621">
<meta name="zs-published" content="20221219161621">
<meta name="zs-visibility" content="public">

Changes to docs/manual/00001012920500.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15

16






-
+






+

-

id: 00001012920500
title: Encodings available via the [[API|00001012000000]]
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109104839
modified: 20230316180859

A zettel representation can be encoded in various formats for further processing.

* [[html|00001012920510]]
* [[md|00001012920513]]
* [[sexpr|00001012920516]]
* [[shtml|00001012920525]]
* [[text|00001012920519]]
* [[zjson|00001012920503]] (will be deprecated in v0.11)
* [[zmk|00001012920522]]

Deleted docs/manual/00001012920503.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






































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920503
title: ZJSON Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109104722

**Note**: ZJSON encoding will be deprecated in v0.11
---
A zettel representation that allows to process the syntactic structure of a zettel.
It is a JSON-based encoding format, but different to the structures returned by using the plain [[endpoint|00001012920000]] ''/z/{ID}''.

For an example, take a look at the ZJSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920503?enc=zjson&part=zettel]],
* [[//z/00001012920503?enc=zjson&part=meta]],
* [[//z/00001012920503?enc=zjson&part=content]].

If transferred via HTTP, the content type will be ''application/json''.

A full zettel encoding results in a JSON object with two keys: ''"meta"'' and ''"content"''.
Both values are the same as if you have requested just the appropriate [[part|00001012920800]].

=== Encoding of metadata
Metadata encoding results in a JSON object, where each metadata key is mapped to the same JSON object name.
The associated value is itself a JSON object with two names.
The first name ``""`` references the [[metadata key type|00001006030000]].
Depending on the key type, the other name denotes the value of the metadata element.
The meaning of these names is [[well defined|00001012920582]], as well as the [[mapping of key types to used object names|00001012920584]].

=== Encoding of zettel content
The content encoding results in a JSON array of objects, where each objects represents a Zettelmarkup element.

Every [!zettelmarkup|Zettelmarkup] element is encoded as a JSON object.
These objects always contain the empty name ''""'' with a string value describing the type of Zettelmarkup element.
Depending on the type, other one letter names denotes the details of the element.
The meaning of these names is [[well defined|00001012920588]].

Changes to docs/manual/00001012920516.zettel.

1
2
3
4
5
6
7

8
9
10
11
12

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


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

7
8
9
10


11

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






-
+



-
-
+
-













+
+







id: 00001012920516
title: Sexpr Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220422181104
modified: 20230109104812
modified: 20230316182622

A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]] (also known as symbolic expression).

It is an alternative to the [[ZJSON encoding|00001012920503]][^ZJSON will be deprecated in v0.11].
Both encodings are (relatively) easy to parse and contain all relevant information of a zettel, metadata and content.
It is (relatively) easy to parse and contain all relevant information of a zettel, metadata and content.

For example, take a look at the Sexpr encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920516?enc=sexpr&part=zettel]],
* [[//z/00001012920516?enc=sexpr&part=meta]],
* [[//z/00001012920516?enc=sexpr&part=content]].

If transferred via HTTP, the content type will be ''text/plain''.

=== Syntax of s-expressions
There are only two types of elements: atoms and lists.

A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.
Before the last element of a list of at least to elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements.
This allows a more space economic storage of data.

There are three syntactic forms for an atom: numbers, symbols and strings.

A number is a non-empty sequence of digits (""0"" ... ""9"").
The smallest number is ``0``, there are no negative numbers.

A symbol is a non-empty sequence of printable characters, except left or right parenthesis.

Added docs/manual/00001012920525.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012920525
title: SHTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230316181044
modified: 20230316182614

A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]], syntactically similar to the [[Sexpr encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics.
It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]].

It is (relatively) easy to parse and contains everything to transform it into real HTML.
In contrast to HTML, SHTML is easier to parse and to manipulate.
For example, take a look at the SHTML encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920525?enc=shtml&part=zettel]],
* [[//z/00001012920525?enc=shtml&part=meta]],
* [[//z/00001012920525?enc=shtml&part=content]].

If transferred via HTTP, the content type will be ''text/plain''.

Internally, if a zettel should be transformed into HTML, the zettel is translated into the [[Sexpr encoding|00001012920516]], which is transformed into this SHTML encoding to produce the [[HTML encoding|00001012920510]].

=== Syntax of SHTML
There are only two types of elements: atoms and lists, similar to the Sexpr encoding.

A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.
Before the last element of a list of at least to elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements.
This allows a more space economic storage of data.

An HTML tag like ``< a href="link">Text</a>`` is encoded in SHTML with a list, where the first element is a symbol named a the tag.
The second element is an optional encoding of the tag's attributes.
Further elements are either other tag encodings or a string.
The above tag is encoded as ``(a (@ (href . "link")) "Text")``.
Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character).

Deleted docs/manual/00001012920582.zettel.

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
















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920582
title: ZJSON Encoding: List of Valid Metadata Value Objects Names
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20230109104713

**Note**: ZJSON encoding will be deprecated in v0.11
---
Every Metadata value element is mapped to a JSON object with some well defined names / keys.

|=Name | JSON Value | Meaning
| ''"\"'' | string | The type of the Zettelmarkup element.
| ''"i"'' | array  | A sequence of [[inline-structured|00001007040000]] elements.
| ''"s"'' | string | The first / major string value of an element.
| ''"y"'' | array  | A set of string values.

Deleted docs/manual/00001012920584.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






























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920584
title: ZJSON Encoding: Mapping of Metadata Key Types to Object Names
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20230109104703

**Note**: ZJSON encoding will be deprecated in v0.11
---
Every [[Metadata key|00001006030000]] is mapped to an [[object name|00001012920582]] where its value is encoded.

|=Type | JSON Object Name | Remark
| [[Credential|00001006031000]] | ''"s"'' | A string with the decrypted credential.
| [[EString|00001006031500]] | ''"s"'' | A possibly empty string.
| [[Identifier|00001006032000]] | ''"s"'' | A string containing a [[zettel identifier|00001006050000]].
| [[IdentifierSet|00001006032500]] | ''"y"'' | An array of strings containing [[zettel identifier|00001006050000]].
| [[Number|00001006033000]] | ''"s"'' | A string containing a numeric value.
| [[String|00001006033500]] | ''"s"'' | A non-empty string.
| [[TagSet|00001006034000]] | ''"y"'' | An array of string containing zettel tags.
| [[Timestamp|00001006034500]] | ''"s"''  | A string containing a timestamp in the format YYYYMMDDHHmmSS.
| [[URL|00001006035000]] | ''"s"'' | A string containing an URL.
| [[Word|00001006035500]] | ''"s"'' | A string containing a word (no space characters)
| [[WordSet|00001006036000]] | ''"y"'' | An array of strings containing words.
| [[Zettelmarkup|00001006036500]] | ''"i"'' | A sequence of [[inline-structured|00001007040000]] elements.

Please note, that metadata is weakly typed.
Every metadata key expects a certain type.
But the user is free to enter something different.
For example, even if the metadata type is ""number"", its value could still be ""abc"".
However, the mapping itself is always valid.

Deleted docs/manual/00001012920588.zettel.

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



























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920588
title: ZJSON Encoding: List of Valid Zettelmarkup Element Objects Names
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20230109104648

**Note**: ZJSON encoding will be deprecated in v0.11
---

Every [[Zettelmarkup|00001007000000]] element is mapped to a JSON object with some well defined names / keys.

|=Name | JSON Value | Meaning
| ''"\"'' | string | The type of the Zettelmarkup element.
| ''"a"'' | object | Additional attributes of the element.
| ''"b"'' | array  | A sequence of [[block-structured|00001007030000]] elements.
| ''"c"'' | array  | A sequence of a sequence of (sub-) list elements or [[inline-structured|00001007040000]] elements. Used for nested lists.
| ''"d"'' | array  | A sequence of description list elements, where each element is an object of a definition term and a list of descriptions.
| ''"e"'' | array  | A sequence of descriptions: a JSON array of simple description, which is itself a JSON array of block structured elements.
| ''"i"'' | array  | A sequence of [[inline-structured|00001007040000]] elements.
| ''"j"'' | object | An objects describing a BLOB element.
| ''"n"'' | number | A numeric value, e.g. for specifying the [[heading|00001007030300]] level.
| ''"o"'' | string | A base64 encoded binary value. Used in some BLOB elements.
| ''"p"'' | array  | A sequence of two elements: a sequence of [[table|00001007031000]] header value, followed by a sequence of sequence of table row values.
| ''"q"'' | string | A second string value, if ''""s""'' is already used.
| ''"s"'' | string | The first / major string value of an element.
| ''"v"'' | string | A third string value, if ''""q""'' is already used.

Changes to domain/content.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/content_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/id/id.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/id/id_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/id/set.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to domain/id/set_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
131
132
133
134
135
136
137
138
139
140
141
142
143






144
145
146
147
148
149
131
132
133
134
135
136
137






138
139
140
141
142
143
144
145
146
147
148
149







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






		got := newS1.Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

// func BenchmarkSet(b *testing.B) {
// 	s := id.Set{}
// 	for i := 0; i < b.N; i++ {
// 		s[id.Zid(i)] = true
// 	}
// }
//	func BenchmarkSet(b *testing.B) {
//		s := id.Set{}
//		for i := 0; i < b.N; i++ {
//			s[id.Zid(i)] = true
//		}
//	}
func BenchmarkSet(b *testing.B) {
	s := id.Set{}
	for i := 0; i < b.N; i++ {
		s[id.Zid(i)] = struct{}{}
	}
}

Changes to domain/id/slice.go.

1
2

3
4
5
6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
1

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

14
15
16
17
18
19
20
21
22

-
+











-

+







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

package id

import (
	"bytes"
	"sort"
	"strings"
)

// Slice is a sequence of zettel identifier. A special case is a sorted slice.
type Slice []Zid

func (zs Slice) Len() int           { return len(zs) }
func (zs Slice) Less(i, j int) bool { return zs[i] < zs[j] }
52
53
54
55
56
57
58
59

60
61
62

63
64

65
66

67
52
53
54
55
56
57
58

59
60
61

62
63

64
65

66
67







-
+


-
+

-
+

-
+

	return true
}

func (zs Slice) String() string {
	if len(zs) == 0 {
		return ""
	}
	var buf bytes.Buffer
	var sb strings.Builder
	for i, zid := range zs {
		if i > 0 {
			buf.WriteByte(' ')
			sb.WriteByte(' ')
		}
		buf.WriteString(zid.String())
		sb.WriteString(zid.String())
	}
	return buf.String()
	return sb.String()
}

Changes to domain/id/slice_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to domain/meta/collection.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to domain/meta/meta.go.

1
2

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

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

15
16
17
18
19
20
21

-
+












-







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

// Package meta provides the domain specific type 'meta'.
package meta

import (
	"bytes"
	"regexp"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"zettelstore.de/c/api"
125
126
127
128
129
130
131
132

133
134
135
136
137
138
139
124
125
126
127
128
129
130

131
132
133
134
135
136
137
138







-
+







	}
	return result
}

// Supported keys.
func init() {
	registerKey(api.KeyID, TypeID, usageComputed, "")
	registerKey(api.KeyTitle, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeyTitle, TypeEmpty, usageUser, "")
	registerKey(api.KeyRole, TypeWord, usageUser, "")
	registerKey(api.KeyTags, TypeTagSet, usageUser, "")
	registerKey(api.KeySyntax, TypeWord, usageUser, "")

	// Properties that are inverse keys
	registerKey(api.KeyFolge, TypeIDSet, usageProperty, "")
	registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "")
401
402
403
404
405
406
407
408

409
410
411
412
413
414
415


416
417
418
419
420
421
422
423


424
400
401
402
403
404
405
406

407
408
409
410
411
412


413
414
415
416
417
418
419
420


421
422
423







-
+





-
-
+
+






-
-
+
+

// RemoveNonGraphic changes the given string not to include non-graphical characters.
// It is needed to sanitize meta data.
func RemoveNonGraphic(s string) string {
	if s == "" {
		return ""
	}
	pos := 0
	var buf bytes.Buffer
	var sb strings.Builder
	for pos < len(s) {
		nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) })
		if nextPos < 0 {
			break
		}
		buf.WriteString(s[pos:nextPos])
		buf.WriteByte(' ')
		sb.WriteString(s[pos:nextPos])
		sb.WriteByte(' ')
		_, size := utf8.DecodeRuneInString(s[nextPos:])
		pos = nextPos + size
	}
	if pos == 0 {
		return strings.TrimSpace(s)
	}
	buf.WriteString(s[pos:])
	return strings.TrimSpace(buf.String())
	sb.WriteString(s[pos:])
	return strings.TrimSpace(sb.String())
}

Changes to domain/meta/meta_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/meta/parse.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/meta/parse_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/meta/type.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/meta/type_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/meta/values.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/meta/write.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to domain/meta/write_test.go.

1
2

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

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

14
15
16
17
18
19
20

-
+











-







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

package meta_test

import (
	"bytes"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)
33
34
35
36
37
38
39
40
41
42



43
44
45
46
47
48
49
32
33
34
35
36
37
38



39
40
41
42
43
44
45
46
47
48







-
-
-
+
+
+







	if syntax != "" {
		m.Set(api.KeySyntax, syntax)
	}
	return m
}
func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
	t.Helper()
	var buf bytes.Buffer
	m.Write(&buf)
	if got := buf.String(); got != expected {
	var sb strings.Builder
	m.Write(&sb)
	if got := sb.String(); got != expected {
		t.Errorf("\nExp: %q\ngot: %q", expected, got)
	}
}

func TestWriteMeta(t *testing.T) {
	t.Parallel()
	assertWriteMeta(t, newMeta("", nil, ""), "")

Changes to domain/zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to encoder/encoder.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to encoder/encoder_blob_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
36
37
38
39
40
41
42
43
44
45


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

43

44
45
46
47
48
49
50
51
52







-

-
+
+







			0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
			0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b,
			0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00,
			0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
			0x42, 0x60, 0x82,
		},
		expect: expectMap{
			encoderZJSON: `[{"":"BLOB","q":[{"":"Text","s":"PNG"}],"s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`,
			encoderHTML:  `<p><img alt="PNG" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="></p>`,
			encoderSexpr: `((BLOB ((TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`,
			encoderSexpr: `(BLOCK (BLOB (INLINE (TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`,
			encoderSHTML: `((p (img (@ (alt . "PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`,
			encoderText:  "",
			encoderZmk:   `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`,
		},
	},
}

func TestBlob(t *testing.T) {

Changes to encoder/encoder_block_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
86
87
88
89
90
91
92
93


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


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














118
119
120
121
122
123
124
125
126
127
128
129


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


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


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


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


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


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


214
215
216
217
218
219
220
221
222
223
224
225


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


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


250
251
252
253
254
255
256
257
258
259
260
261


262
263
264
265
266
267
268
269
270
271













272
273


274
275
276
277
278
279
280
281
282
283
284
285


286
287
288
289
290
291
292
293
294
295
296
297
298
299
300


301
302
303
304
305
306
307
308
309


310





311

312







313
314
315



316
317
318
319
320
321
322
323
324
325
326


327
328
329
330
331
332
333
334
335
336
337
338
339





340
341
342
343
344
345
346
347
348


349
350
351
352
353
354
355
356
357
1

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

18
19

20
21
22
23
24
25
26
27
28
29

30
31

32
33
34
35
36
37
38
39
40
41

42
43

44
45
46
47
48
49
50
51
52
53


54
55

56
57
58
59
60
61
62
63
64
65

66
67

68
69
70
71
72
73
74
75
76
77

78
79

80
81
82
83
84
85
86
87
88
89

90
91

92
93
94
95
96
97
98
99
100
101

102
103

104
105
106
107
108
109
110
111
112
113

114
115

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

138
139

140
141
142
143
144
145
146
147
148
149

150
151

152
153
154
155
156
157
158
159
160
161

162
163

164
165
166
167
168
169
170
171
172
173

174
175

176
177
178
179
180
181
182
183
184
185

186
187

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

206
207

208
209
210
211
212
213
214
215
216
217
218
219
220
221

222
223

224
225
226
227
228
229
230
231
232
233

234
235

236
237
238
239
240
241
242
243
244
245

246
247

248
249
250
251
252
253
254
255
256
257

258
259

260
261
262
263
264
265
266
267
268
269

270
271

272
273
274
275
276
277
278
279
280
281


282
283
284
285
286
287
288
289
290
291
292
293
294
295

296
297
298
299
300
301
302
303
304
305

306
307

308
309
310
311
312
313
314
315
316
317
318
319
320

321
322

323
324
325
326
327
328
329
330
331


332
333
334
335
336
337
338
339

340

341
342
343
344
345
346
347
348


349
350
351
352
353
354
355
356
357
358

359
360

361
362
363
364
365
366
367
368
369
370





371
372
373
374
375
376
377
378
379
380
381

382

383
384
385
386
387
388
389
390
391
392
393

-
+















-


-
+
+








-


-
+
+








-


-
+
+








-
-
+

-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


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








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+
















-


-
+
+












-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








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

-
+
+








-


-
+
+











-


-
+
+







-
-
+
+

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

-
-
+
+
+







-


-
+
+








-
-
-
-
-
+
+
+
+
+






-

-
+
+









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

package encoder_test

var tcsBlock = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderMD:    "",
			encoderSexpr: `()`,
			encoderSexpr: `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world",
		zmk:   "Hello, world",
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]}]`,
			encoderHTML:  "<p>Hello, world</p>",
			encoderMD:    "Hello, world",
			encoderSexpr: `((PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`,
			encoderSexpr: `(BLOCK (PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`,
			encoderSHTML: `((p "Hello," " " "world"))`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple block comment",
		zmk:   "%%%\nNo\nrender\n%%%",
		expect: expectMap{
			encoderZJSON: `[{"":"CommentBlock","s":"No\nrender"}]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-COMMENT () "No\nrender"))`,
			encoderSexpr: `(BLOCK (VERBATIM-COMMENT () "No\nrender"))`,
			encoderSHTML: `(())`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Rendered block comment",
		zmk:   "%%%{-}\nRender\n%%%",
		expect: expectMap{
			encoderZJSON: `[{"":"CommentBlock","a":{"-":""},"s":"Render"}]`,
			encoderHTML:  "<!--\nRender\n-->",
			encoderHTML:  "<!--\nRender\n-->\n",
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-COMMENT (("-" "")) "Render"))`,
			encoderSexpr: `(BLOCK (VERBATIM-COMMENT (quote (("-" . ""))) "Render"))`,
			encoderSHTML: "((@@@ \"Render\"))",
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Heading",
		zmk:   `=== Top`,
		expect: expectMap{
			encoderZJSON: `[{"":"Heading","n":1,"s":"top","i":[{"":"Text","s":"Top"}]}]`,
			encoderHTML:  "<h2 id=\"top\">Top</h2>",
			encoderMD:    "# Top",
			encoderSexpr: `((HEADING 1 () "top" "top" (TEXT "Top")))`,
			encoderSexpr: `(BLOCK (HEADING 1 () "top" "top" (INLINE (TEXT "Top"))))`,
			encoderSHTML: `((h2 (@ (id . "top")) "Top"))`,
			encoderText:  `Top`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List",
		zmk:   "* A\n* B\n* C",
		expect: expectMap{
			encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"A"}]}],[{"":"Para","i":[{"":"Text","s":"B"}]}],[{"":"Para","i":[{"":"Text","s":"C"}]}]]}]`,
			encoderHTML:  "<ul><li>A</li><li>B</li><li>C</li></ul>",
			encoderMD:    "* A\n* B\n* C",
			encoderSexpr: `((UNORDERED ((TEXT "A")) ((TEXT "B")) ((TEXT "C"))))`,
			encoderSexpr: `(BLOCK (UNORDERED (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`,
			encoderSHTML: `((ul (li "A") (li "B") (li "C")))`,
			encoderText:  "A\nB\nC",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested List",
		zmk:   "* T1\n** T2\n* T3\n** T4\n** T5\n* T6",
		expect: expectMap{
			encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T1"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T2"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T3"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T4"}]}],[{"":"Para","i":[{"":"Text","s":"T5"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T6"}]}]]}]`,
			encoderHTML:  `<ul><li><p>T1</p><ul><li>T2</li></ul></li><li><p>T3</p><ul><li>T4</li><li>T5</li></ul></li><li><p>T6</p></li></ul>`,
			encoderMD:    "* T1\n    * T2\n* T3\n    * T4\n    * T5\n* T6",
			encoderSexpr: `((UNORDERED ((PARA (TEXT "T1")) (UNORDERED ((TEXT "T2")))) ((PARA (TEXT "T3")) (UNORDERED ((TEXT "T4")) ((TEXT "T5")))) ((PARA (TEXT "T6")))))`,
			encoderSexpr: `(BLOCK (UNORDERED (BLOCK (PARA (TEXT "T1")) (UNORDERED (INLINE (TEXT "T2")))) (BLOCK (PARA (TEXT "T3")) (UNORDERED (INLINE (TEXT "T4")) (INLINE (TEXT "T5")))) (BLOCK (PARA (TEXT "T6")))))`,
			encoderSHTML: `((ul (li (p "T1") (ul (li "T2"))) (li (p "T3") (ul (li "T4") (li "T5"))) (li (p "T6"))))`,
			encoderText:  "T1\nT2\nT3\nT4\nT5\nT6",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Sequence of two lists",
		zmk:   "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2",
		expect: expectMap{
			encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"Item1.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.2"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.3"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.2"}]}]]}]`,
			encoderHTML:  "<ul><li>Item1.1</li><li>Item1.2</li><li>Item1.3</li><li>Item2.1</li><li>Item2.2</li></ul>",
			encoderMD:    "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2",
			encoderSexpr: `((UNORDERED ((TEXT "Item1.1")) ((TEXT "Item1.2")) ((TEXT "Item1.3")) ((TEXT "Item2.1")) ((TEXT "Item2.2"))))`,
			encoderSexpr: `(BLOCK (UNORDERED (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`,
			encoderSHTML: `((ul (li "Item1.1") (li "Item1.2") (li "Item1.3") (li "Item2.1") (li "Item2.2")))`,
			encoderText:  "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2",
			encoderZmk:   "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2",
		},
	},
	{
		descr: "Simple horizontal rule",
		zmk:   `---`,
		expect: expectMap{
			encoderZJSON: `[{"":"Thematic"}]`,
			encoderHTML:  "<hr>",
			encoderMD:    "---",
			encoderSexpr: `((THEMATIC ()))`,
			encoderSexpr: `(BLOCK (THEMATIC ()))`,
			encoderSHTML: `((hr))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Thematic break with attribute",
		zmk:   `---{lang="zmk"}`,
		expect: expectMap{
			encoderHTML:  `<hr lang="zmk">`,
			encoderMD:    "---",
			encoderSexpr: `(BLOCK (THEMATIC (quote (("lang" . "zmk")))))`,
			encoderSHTML: `((hr (@ (lang . "zmk"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "No list after paragraph",
		zmk:   "Text\n*abc",
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Soft"},{"":"Text","s":"*abc"}]}]`,
			encoderHTML:  "<p>Text *abc</p>",
			encoderMD:    "Text\n*abc",
			encoderSexpr: `((PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`,
			encoderSexpr: `(BLOCK (PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`,
			encoderSHTML: `((p "Text" " " "*abc"))`,
			encoderText:  `Text *abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "A list after paragraph",
		zmk:   "Text\n# abc",
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"}]},{"":"Ordered","c":[[{"":"Para","i":[{"":"Text","s":"abc"}]}]]}]`,
			encoderHTML:  "<p>Text</p><ol><li>abc</li></ol>",
			encoderMD:    "Text\n\n1. abc",
			encoderSexpr: `((PARA (TEXT "Text")) (ORDERED ((TEXT "abc"))))`,
			encoderSexpr: `(BLOCK (PARA (TEXT "Text")) (ORDERED (INLINE (TEXT "abc"))))`,
			encoderSHTML: `((p "Text") (ol (li "abc")))`,
			encoderText:  "Text\nabc",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List Quote",
		zmk:   "> ToBeOrNotToBe",
		expect: expectMap{
			encoderZJSON: `[{"":"Quotation","c":[[{"":"Para","i":[{"":"Text","s":"ToBeOrNotToBe"}]}]]}]`,
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p></blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSexpr: `((QUOTATION ((TEXT "ToBeOrNotToBe"))))`,
			encoderSexpr: `(BLOCK (QUOTATION (INLINE (TEXT "ToBeOrNotToBe"))))`,
			encoderSHTML: `((blockquote (p "ToBeOrNotToBe")))`,
			encoderText:  "ToBeOrNotToBe",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Quote Block",
		zmk:   "<<<\nToBeOrNotToBe\n<<< Romeo",
		expect: expectMap{
			encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOrNotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`,
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo</cite></blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) ((TEXT "Romeo"))))`,
			encoderSexpr: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOrNotToBe"))) (INLINE (TEXT "Romeo"))))`,
			encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo")))`,
			encoderText:  "ToBeOrNotToBe\nRomeo",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quote Block with multiple paragraphs",
		zmk:   "<<<\nToBeOr\n\nNotToBe\n<<< Romeo",
		expect: expectMap{
			encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOr"}]},{"":"Para","i":[{"":"Text","s":"NotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`,
			encoderHTML:  "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>",
			encoderMD:    "> ToBeOr\n\n> NotToBe",
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) ((TEXT "Romeo"))))`,
			encoderSexpr: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (INLINE (TEXT "Romeo"))))`,
			encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`,
			encoderText:  "ToBeOr\nNotToBe\nRomeo",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Verse block",
		zmk: `"""
A line
  another line
Back

Paragraph

    Spacy  Para
""" Author`,
		expect: expectMap{
			encoderZJSON: "[{\"\":\"Poem\",\"b\":[{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"A\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"another\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Text\",\"s\":\"Back\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"Paragraph\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Space\",\"s\":\"\u00a0\u00a0\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Spacy\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Para\"}]}],\"i\":[{\"\":\"Text\",\"s\":\"Author\"}]}]",
			encoderHTML:  "<div><p>A\u00a0line<br>\u00a0\u00a0another\u00a0line<br>Back</p><p>Paragraph</p><p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p><cite>Author</cite></div>",
			encoderMD:    "",
			encoderSexpr: "((REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) ((TEXT \"Author\"))))",
			encoderSexpr: "(BLOCK (REGION-VERSE () (BLOCK (PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) (INLINE (TEXT \"Author\"))))",
			encoderSHTML: "((div (p \"A\" \"\u00a0\" \"line\" (br) \"\u00a0\u00a0\" \"another\" \"\u00a0\" \"line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0\" \"Spacy\" \"\u00a0\u00a0\" \"Para\") (cite \"Author\")))",
			encoderText:  "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor",
			encoderZmk:   "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author",
		},
	},
	{
		descr: "Span Block",
		zmk: `:::
A simple
   span
and much more
:::`,
		expect: expectMap{
			encoderZJSON: `[{"":"Block","b":[{"":"Para","i":[{"":"Text","s":"A"},{"":"Space"},{"":"Text","s":"simple"},{"":"Soft"},{"":"Space"},{"":"Text","s":"span"},{"":"Soft"},{"":"Text","s":"and"},{"":"Space"},{"":"Text","s":"much"},{"":"Space"},{"":"Text","s":"more"}]}]}]`,
			encoderHTML:  "<div><p>A simple  span and much more</p></div>",
			encoderMD:    "",
			encoderSexpr: `((REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) ()))`,
			encoderSexpr: `(BLOCK (REGION-BLOCK () (BLOCK (PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) (INLINE)))`,
			encoderSHTML: `((div (p "A" " " "simple" " " " " "span" " " "and" " " "much" " " "more")))`,
			encoderText:  `A simple  span and much more`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code",
		zmk:   "```\nHello\nWorld\n```",
		expect: expectMap{
			encoderZJSON: `[{"":"CodeBlock","s":"Hello\nWorld"}]`,
			encoderHTML:  "<pre><code>Hello\nWorld</code></pre>",
			encoderMD:    "    Hello\n    World",
			encoderSexpr: `((VERBATIM-CODE () "Hello\nWorld"))`,
			encoderSexpr: `(BLOCK (VERBATIM-CODE () "Hello\nWorld"))`,
			encoderSHTML: `((pre (code "Hello\nWorld")))`,
			encoderText:  "Hello\nWorld",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code with visible spaces",
		zmk:   "```{-}\nHello World\n```",
		expect: expectMap{
			encoderZJSON: `[{"":"CodeBlock","a":{"-":""},"s":"Hello World"}]`,
			encoderHTML:  "<pre><code>Hello\u2423World</code></pre>",
			encoderMD:    "    Hello World",
			encoderSexpr: `((VERBATIM-CODE (("-" "")) "Hello World"))`,
			encoderSexpr: `(BLOCK (VERBATIM-CODE (quote (("-" . ""))) "Hello World"))`,
			encoderSHTML: "((pre (code \"Hello\u2423World\")))",
			encoderText:  "Hello World",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Eval",
		zmk:   "~~~\nHello\nWorld\n~~~",
		expect: expectMap{
			encoderZJSON: `[{"":"EvalBlock","s":"Hello\nWorld"}]`,
			encoderHTML:  "<pre><code class=\"zs-eval\">Hello\nWorld</code></pre>",
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-EVAL () "Hello\nWorld"))`,
			encoderSexpr: `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`,
			encoderSHTML: "((pre (code (@ (class . \"zs-eval\")) \"Hello\\nWorld\")))",
			encoderText:  "Hello\nWorld",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Math",
		zmk:   "$$$\nHello\n\\LaTeX\n$$$",
		expect: expectMap{
			encoderZJSON: `[{"":"MathBlock","s":"Hello\n\\LaTeX"}]`,
			encoderHTML:  "<pre><code class=\"zs-math\">Hello\n\\LaTeX</code></pre>",
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-MATH () "Hello\n\\LaTeX"))`,
			encoderSexpr: `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`,
			encoderSHTML: "((pre (code (@ (class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))",
			encoderText:  "Hello\n\\LaTeX",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderZJSON: `[{"":"Description","d":[{"i":[{"":"Text","s":"Zettel"}],"e":[[{"":"Para","i":[{"":"Text","s":"Paper"}]}],[{"":"Para","i":[{"":"Text","s":"Note"}]}]]},{"i":[{"":"Text","s":"Zettelkasten"}],"e":[[{"":"Para","i":[{"":"Text","s":"Slip"},{"":"Space"},{"":"Text","s":"box"}]}]]}]}]`,
			encoderHTML:  "<dl><dt>Zettel</dt><dd>Paper</dd><dd>Note</dd><dt>Zettelkasten</dt><dd>Slip box</dd></dl>",
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p></dd><dd><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSexpr: `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with paragraphs as item",
		zmk:   "; Zettel\n: Paper\n\n  Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSexpr: `((DESCRIPTION ((TEXT "Zettel")) (((TEXT "Paper")) ((TEXT "Note"))) ((TEXT "Zettelkasten")) (((TEXT "Slip") (SPACE) (TEXT "box")))))`,
			encoderSexpr: `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Table",
		zmk:   "|c1|c2|c3\n|d1||d3",
		expect: expectMap{
			encoderZJSON: `[{"":"Table","p":[[],[[{"i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"i":[{"":"Text","s":"c3"}]}],[{"i":[{"":"Text","s":"d1"}]},{"i":[]},{"i":[{"":"Text","s":"d3"}]}]]]}]`,
			encoderHTML:  `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSexpr: `((TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,
			encoderSexpr: `(BLOCK (TABLE () (LIST (CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) (LIST (CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,
			encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`,
			encoderText:  "c1 c2 c3\nd1  d3",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Table with alignment and comment",
		zmk: `|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3`,
		expect: expectMap{
			encoderZJSON: `[{"":"Table","p":[[{"s":">","i":[{"":"Text","s":"h1"}]},{"i":[{"":"Text","s":"h2"}]},{"s":":","i":[{"":"Text","s":"h3"}]}],[[{"s":"<","i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"s":":","i":[{"":"Text","s":"c3"}]}],[{"s":">","i":[{"":"Text","s":"f1"}]},{"i":[{"":"Text","s":"f2"}]},{"s":":","i":[{"":"Text","s":"=f3"}]}]]]}]`,
			encoderHTML:  `<table><thead><tr><td class="right">h1</td><td>h2</td><td class="center">h3</td></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSexpr: `((TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`,
			encoderSexpr: `(BLOCK (TABLE (LIST (CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) (LIST (CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) (LIST (CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`,
			encoderSHTML: `((table (thead (tr (td (@ (class . "right")) "h1") (td "h2") (td (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`,
			encoderText:  "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: `|=h1>|=h2|=h3:
|<c1|c2|c3
|f1|f2|=f3`,
		},
	},
	{
		descr: "Simple Endnotes",
		zmk:   `Text[^Footnote]`,
		descr: "Simple Endnote",
		zmk:   `Text[^Endnote]`,
		expect: expectMap{
			encoderHTML:  "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>",
			encoderMD:    "Text",
			encoderSexpr: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (quote (INLINE (TEXT "Endnote"))))))`,
			encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderText:  "Text Endnote",
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Footnote","i":[{"":"Text","s":"Footnote"}]}]}]`,
			encoderZmk:   useZmk,
			encoderHTML:  `<p>Text<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup></p><ol class="zs-endnotes"><li class="zs-endnote" id="fn:1" role="doc-endnote" value="1">Footnote <a class="zs-endnote-backref" href="#fnref:1" role="doc-backlink">&#x21a9;&#xfe0e;</a></li></ol>`,
		},
	},
	{
		descr: "Nested Endnotes",
		zmk:   `Text[^Endnote[^Nested]]`,
		expect: expectMap{
			encoderHTML:  "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote<sup id=\"fnref:2\"><a class=\"zs-noteref\" href=\"#fn:2\" role=\"doc-noteref\">2</a></sup> <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li><li class=\"zs-endnote\" id=\"fn:2\" role=\"doc-endnote\" value=\"2\">Nested <a class=\"zs-endnote-backref\" href=\"#fnref:2\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>",
			encoderMD:    "Text",
			encoderSexpr: `((PARA (TEXT "Text") (FOOTNOTE () (TEXT "Footnote"))))`,
			encoderText:  "Text Footnote",
			encoderSexpr: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (quote (INLINE (TEXT "Endnote") (ENDNOTE () (quote (INLINE (TEXT "Nested")))))))))`,
			encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderText:  "Text Endnote Nested",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Transclusion",
		zmk:   `{{{http://example.com/image}}}{width="100px"}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Transclude","a":{"width":"100px"},"q":"external","s":"http://example.com/image"}]`,
			encoderHTML:  `<p><img class="external" src="http://example.com/image" width="100px"></p>`,
			encoderMD:    "",
			encoderSexpr: `((TRANSCLUDE (("width" "100px")) (EXTERNAL "http://example.com/image")))`,
			encoderSexpr: `(BLOCK (TRANSCLUDE (quote (("width" . "100px"))) (quote (EXTERNAL "http://example.com/image"))))`,
			encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "A paragraph with a inline comment only should be empty in HTML",
		zmk:   `%% Comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Comment","s":"Comment"}]}]`,
			encoderHTML:  ``,
			encoderSexpr: `((PARA (LITERAL-COMMENT () "Comment")))`,
			encoderText:  "",
			encoderZmk:   useZmk,
			// encoderHTML:  ``,
			encoderSexpr: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`,
			// encoderSHTML: ``,
			encoderText: "",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderSexpr: `()`,
			encoderSexpr: `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
}

// func TestEncoderBlock(t *testing.T) {
// 	executeTestCases(t, tcsBlock)
// }

Changes to encoder/encoder_inline_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
86
87
88
89
90
91
92
93


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


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


118
119
120
121
122
123
124
125
126
127
128
129


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


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


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


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


178
179
180
181
182
183
184
185
186
187
188
189


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


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


214
215
216
217
218
219
220
221
222
223
224
225


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


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


250
251
252
253
254











255
256
257
258
259
260
261


262
263
264
265
266
267
268
269
270
271
272
273


274
275
276
277
278
279
280
281
282
283
284

285

286
287
288
289
290
291
292
293
294
295
296
297


298
299
300
301
302
303
304
305
306
307

308
309


310
311
312
313
314
315
316


317
318
319
320
321
322



323
324
325
326
327
328
329
330
331
332
333


334
335
336
337
338
339
340
341
342
343
344
345


346
347
348
349
























350
351
352
353
354
355
356
357


358
359
360
361
362
363
364
365
366
367
368
369


370
371
372
373
374
375
376
377
378
379
380
381


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


394
395
396
397
398
399
400
401
402
403
404
405


406
407
408
409
410
411
412
413
414
415
416
417


418
419
420
421
422
423
424
425
426
427
428
429


430
431
432
433
434
435
436
437
438
439
440
441


442
443
444
445
446
447
448
449
450
451
452
453


454
455
456
457
458
459
460

461
462
463
464
465

466

467
468
469
470
471
472
473
474
475
476
477


478
479
480
481
482
483
484
485
486
487
488
489


490
491
492
493
494
495
496
497
498
499
500
501


502
503
504
505
506
507
508
509
510
511

512
513


514
515
516
517
518
519
520
521
522
523
524
525


526
527
528
529
530
531
532
533
534
535
536
537


538
539
540
541
542
543
544
545
546
547
548
549


550
551
552
553
554
1

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

18
19

20
21
22
23
24
25
26
27
28
29

30
31

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

66
67

68
69
70
71
72
73
74
75
76
77

78
79

80
81
82
83
84
85
86
87
88
89

90
91

92
93
94
95
96
97
98
99
100
101

102
103

104
105
106
107
108
109
110
111
112
113

114
115

116
117
118
119
120
121
122
123
124
125

126
127

128
129
130
131
132
133
134
135
136
137

138
139

140
141
142
143
144
145
146
147
148
149

150
151

152
153
154
155
156
157
158
159
160
161

162
163

164
165
166
167
168
169
170
171
172
173

174
175

176
177
178
179
180
181
182
183
184
185

186
187

188
189
190
191
192
193
194
195
196
197

198
199

200
201
202
203
204
205
206
207
208
209

210
211

212
213
214
215
216
217
218
219
220
221

222
223

224
225
226
227
228
229
230
231
232
233

234
235

236
237
238
239
240
241
242
243
244
245

246
247

248
249
250
251
252
253
254
255
256
257

258
259

260
261
262
263
264
265
266
267
268
269

270
271

272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292

293
294

295
296
297
298
299
300
301
302
303
304

305
306

307
308
309
310
311
312
313
314
315
316

317
318
319

320
321
322
323
324
325
326
327
328

329
330

331
332
333
334
335
336
337
338
339
340


341
342

343
344
345
346
347
348
349


350
351
352

353
354


355
356
357
358
359
360
361
362
363
364

365
366

367
368
369
370
371
372
373
374
375
376

377
378

379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412

413
414

415
416
417
418
419
420
421
422
423
424

425
426

427
428
429
430
431
432
433
434
435
436

437
438

439
440
441
442
443
444
445
446
447
448

449
450

451
452
453
454
455
456
457
458
459
460

461
462

463
464
465
466
467
468
469
470
471
472

473
474

475
476
477
478
479
480
481
482
483
484

485
486

487
488
489
490
491
492
493
494
495
496

497
498

499
500
501
502
503
504
505
506
507
508

509
510

511
512
513
514
515
516
517
518

519
520

521
522

523
524
525
526
527
528
529
530
531
532

533
534

535
536
537
538
539
540
541
542
543
544

545
546

547
548
549
550
551
552
553
554
555
556

557
558

559
560
561
562
563
564
565
566
567
568


569
570

571
572
573
574
575
576
577
578
579
580

581
582

583
584
585
586
587
588
589
590
591
592

593
594

595
596
597
598
599
600
601
602
603
604

605
606

607
608
609
610
611
612
613

-
+















-


-
+
+








-


-
+
+




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




-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+





+
+
+
+
+
+
+
+
+
+
+



-


-
+
+








-


-
+
+








-


+
-
+








-


-
+
+








-
-
+

-
+
+





-
-
+
+

-


-
-
+
+
+







-


-
+
+








-


-
+
+




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




-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+






-
+

-


-
+

+







-


-
+
+








-


-
+
+








-


-
+
+








-
-
+

-
+
+








-


-
+
+








-


-
+
+








-


-
+
+





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

package encoder_test

var tcsInline = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing (inline)",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderMD:    "",
			encoderSexpr: `()`,
			encoderSexpr: `(INLINE)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world (inline)",
		zmk:   `Hello, world`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]`,
			encoderHTML:  "Hello, world",
			encoderMD:    "Hello, world",
			encoderSexpr: `((TEXT "Hello,") (SPACE) (TEXT "world"))`,
			encoderSexpr: `(INLINE (TEXT "Hello,") (SPACE) (TEXT "world"))`,
			encoderSHTML: `("Hello," " " "world")`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Soft Break",
		zmk:   "soft\nbreak",
		expect: expectMap{
			encoderHTML:  "soft break",
			encoderMD:    "soft\nbreak",
			encoderSexpr: `(INLINE (TEXT "soft") (SOFT) (TEXT "break"))`,
			encoderSHTML: `("soft" " " "break")`,
			encoderText:  "soft break",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Hard Break",
		zmk:   "hard\\\nbreak",
		expect: expectMap{
			encoderHTML:  "hard<br>break",
			encoderMD:    "hard\\\nbreak",
			encoderSexpr: `(INLINE (TEXT "hard") (HARD) (TEXT "break"))`,
			encoderSHTML: `("hard" (br) "break")`,
			encoderText:  "hard\nbreak",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Emphasized formatting",
		zmk:   "__emph__",
		expect: expectMap{
			encoderZJSON: `[{"":"Emph","i":[{"":"Text","s":"emph"}]}]`,
			encoderHTML:  "<em>emph</em>",
			encoderMD:    "*emph*",
			encoderSexpr: `((FORMAT-EMPH () (TEXT "emph")))`,
			encoderSexpr: `(INLINE (FORMAT-EMPH () (TEXT "emph")))`,
			encoderSHTML: `((em "emph"))`,
			encoderText:  "emph",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Strong formatting",
		zmk:   "**strong**",
		expect: expectMap{
			encoderZJSON: `[{"":"Strong","i":[{"":"Text","s":"strong"}]}]`,
			encoderHTML:  "<strong>strong</strong>",
			encoderMD:    "__strong__",
			encoderSexpr: `((FORMAT-STRONG () (TEXT "strong")))`,
			encoderSexpr: `(INLINE (FORMAT-STRONG () (TEXT "strong")))`,
			encoderSHTML: `((strong "strong"))`,
			encoderText:  "strong",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Insert formatting",
		zmk:   ">>insert>>",
		expect: expectMap{
			encoderZJSON: `[{"":"Insert","i":[{"":"Text","s":"insert"}]}]`,
			encoderHTML:  "<ins>insert</ins>",
			encoderMD:    "insert",
			encoderSexpr: `((FORMAT-INSERT () (TEXT "insert")))`,
			encoderSexpr: `(INLINE (FORMAT-INSERT () (TEXT "insert")))`,
			encoderSHTML: `((ins "insert"))`,
			encoderText:  "insert",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Delete formatting",
		zmk:   "~~delete~~",
		expect: expectMap{
			encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"delete"}]}]`,
			encoderHTML:  "<del>delete</del>",
			encoderMD:    "delete",
			encoderSexpr: `((FORMAT-DELETE () (TEXT "delete")))`,
			encoderSexpr: `(INLINE (FORMAT-DELETE () (TEXT "delete")))`,
			encoderSHTML: `((del "delete"))`,
			encoderText:  "delete",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Update formatting",
		zmk:   "~~old~~>>new>>",
		expect: expectMap{
			encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"old"}]},{"":"Insert","i":[{"":"Text","s":"new"}]}]`,
			encoderHTML:  "<del>old</del><ins>new</ins>",
			encoderMD:    "oldnew",
			encoderSexpr: `((FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`,
			encoderSexpr: `(INLINE (FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`,
			encoderSHTML: `((del "old") (ins "new"))`,
			encoderText:  "oldnew",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Superscript formatting",
		zmk:   "^^superscript^^",
		expect: expectMap{
			encoderZJSON: `[{"":"Super","i":[{"":"Text","s":"superscript"}]}]`,
			encoderHTML:  `<sup>superscript</sup>`,
			encoderMD:    "superscript",
			encoderSexpr: `((FORMAT-SUPER () (TEXT "superscript")))`,
			encoderSexpr: `(INLINE (FORMAT-SUPER () (TEXT "superscript")))`,
			encoderSHTML: `((sup "superscript"))`,
			encoderText:  `superscript`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Subscript formatting",
		zmk:   ",,subscript,,",
		expect: expectMap{
			encoderZJSON: `[{"":"Sub","i":[{"":"Text","s":"subscript"}]}]`,
			encoderHTML:  `<sub>subscript</sub>`,
			encoderMD:    "subscript",
			encoderSexpr: `((FORMAT-SUB () (TEXT "subscript")))`,
			encoderSexpr: `(INLINE (FORMAT-SUB () (TEXT "subscript")))`,
			encoderSHTML: `((sub "subscript"))`,
			encoderText:  `subscript`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderZJSON: `[{"":"Quote","i":[{"":"Text","s":"quotes"}]}]`,
			encoderHTML:  "<q>quotes</q>",
			encoderMD:    "<q>quotes</q>",
			encoderSexpr: `((FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSexpr: `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSHTML: `((q "quotes"))`,
			encoderText:  `quotes`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Quote","a":{"lang":"de"},"i":[{"":"Text","s":"quotes"}]}]`,
			encoderHTML:  `<span lang="de"><q>quotes</q></span>`,
			encoderMD:    "<q>quotes</q>",
			encoderSexpr: `((FORMAT-QUOTE (("lang" "de")) (TEXT "quotes")))`,
			encoderSexpr: `(INLINE (FORMAT-QUOTE (quote (("lang" . "de"))) (TEXT "quotes")))`,
			encoderSHTML: `((span (@ (lang . "de")) (q "quotes")))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Span formatting",
		zmk:   `::span::`,
		expect: expectMap{
			encoderZJSON: `[{"":"Span","i":[{"":"Text","s":"span"}]}]`,
			encoderHTML:  `<span>span</span>`,
			encoderMD:    "span",
			encoderSexpr: `((FORMAT-SPAN () (TEXT "span")))`,
			encoderSexpr: `(INLINE (FORMAT-SPAN () (TEXT "span")))`,
			encoderSHTML: `((span "span"))`,
			encoderText:  `span`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Code formatting",
		zmk:   "``code``",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","s":"code"}]`,
			encoderHTML:  `<code>code</code>`,
			encoderMD:    "`code`",
			encoderSexpr: `((LITERAL-CODE () "code"))`,
			encoderSexpr: `(INLINE (LITERAL-CODE () "code"))`,
			encoderSHTML: `((code "code"))`,
			encoderText:  `code`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Code formatting with visible space",
		zmk:   "``x y``{-}",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","a":{"-":""},"s":"x y"}]`,
			encoderHTML:  "<code>x\u2423y</code>",
			encoderMD:    "`x y`",
			encoderSexpr: `((LITERAL-CODE (("-" "")) "x y"))`,
			encoderSexpr: `(INLINE (LITERAL-CODE (quote (("-" . ""))) "x y"))`,
			encoderSHTML: "((code \"x\u2423y\"))",
			encoderText:  `x y`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "HTML in Code formatting",
		zmk:   "``<script `` abc",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","s":"<script "},{"":"Space"},{"":"Text","s":"abc"}]`,
			encoderHTML:  "<code>&lt;script </code> abc",
			encoderMD:    "`<script ` abc",
			encoderSexpr: `((LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`,
			encoderSexpr: `(INLINE (LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`,
			encoderSHTML: `((code "<script ") " " "abc")`,
			encoderText:  `<script  abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Input formatting",
		zmk:   `''input''`,
		expect: expectMap{
			encoderZJSON: `[{"":"Input","s":"input"}]`,
			encoderHTML:  `<kbd>input</kbd>`,
			encoderMD:    "input",
			encoderSexpr: `((LITERAL-INPUT () "input"))`,
			encoderSexpr: `(INLINE (LITERAL-INPUT () "input"))`,
			encoderSHTML: `((kbd "input"))`,
			encoderText:  `input`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Output formatting",
		zmk:   `==output==`,
		expect: expectMap{
			encoderZJSON: `[{"":"Output","s":"output"}]`,
			encoderHTML:  `<samp>output</samp>`,
			encoderMD:    "output",
			encoderSexpr: `((LITERAL-OUTPUT () "output"))`,
			encoderSexpr: `(INLINE (LITERAL-OUTPUT () "output"))`,
			encoderSHTML: `((samp "output"))`,
			encoderText:  `output`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Math formatting",
		zmk:   `$$\TeX$$`,
		expect: expectMap{
			encoderZJSON: `[{"":"Math","s":"\\TeX"}]`,
			encoderHTML:  `<code class="zs-math">\TeX</code>`,
			encoderMD:    "\\TeX",
			encoderSexpr: `((LITERAL-MATH () "\\TeX"))`,
			encoderSexpr: `(INLINE (LITERAL-MATH () "\\TeX"))`,
			encoderSHTML: `((code (@ (class . "zs-math")) "\\TeX"))`,
			encoderText:  `\TeX`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Span","a":{"lang":"fr"},"i":[{"":"Quote","i":[{"":"Text","s":"abc"}]}]}]`,
			encoderHTML:  `<span lang="fr"><q>abc</q></span>`,
			encoderMD:    "<q>abc</q>",
			encoderSexpr: `((FORMAT-SPAN (("lang" "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSexpr: `(INLINE (FORMAT-SPAN (quote (("lang" . "fr"))) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSHTML: `((span (@ (lang . "fr")) (q "abc")))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
		},
	},
	{
		descr: "Simple Citation",
		zmk:   `[@Stern18]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Cite","s":"Stern18"}]`,
			encoderHTML:  `<span>Stern18</span>`, // TODO
			encoderMD:    "",
			encoderSexpr: `((CITE () "Stern18"))`,
			encoderSexpr: `(INLINE (CITE () "Stern18"))`,
			encoderSHTML: `((span "Stern18"))`, // TODO
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Citation",
		zmk:   `[@Stern18 p.23]`,
		expect: expectMap{
			encoderHTML:  `<span>Stern18, p.23</span>`, // TODO
			encoderMD:    "p.23",
			encoderSexpr: `(INLINE (CITE () "Stern18" (TEXT "p.23")))`,
			encoderSHTML: `((span "Stern18" ", " "p.23"))`, // TODO
			encoderText:  `p.23`,
			encoderZmk:   useZmk,
		},
	}, {
		descr: "No comment",
		zmk:   `% comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"%"},{"":"Space"},{"":"Text","s":"comment"}]`,
			encoderHTML:  `% comment`,
			encoderMD:    "% comment",
			encoderSexpr: `((TEXT "%") (SPACE) (TEXT "comment"))`,
			encoderSexpr: `(INLINE (TEXT "%") (SPACE) (TEXT "comment"))`,
			encoderSHTML: `("%" " " "comment")`,
			encoderText:  `% comment`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment (nogen HTML)",
		zmk:   `%% line comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Comment","s":"line comment"}]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `((LITERAL-COMMENT () "line comment"))`,
			encoderSexpr: `(INLINE (LITERAL-COMMENT () "line comment"))`,
			encoderSHTML: `(())`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment",
		zmk:   `%%{-} line comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Comment","a":{"-":""},"s":"line comment"}]`,
			encoderHTML:  `<!-- line comment -->`,
			encoderMD:    "",
			encoderSexpr: `(INLINE (LITERAL-COMMENT (quote (("-" . ""))) "line comment"))`,
			encoderSexpr: `((LITERAL-COMMENT (("-" "")) "line comment"))`,
			encoderSHTML: `((@@ "line comment"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text",
		zmk:   `Text %%{-} comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment"}]`,
			encoderHTML:  `Text<!-- comment -->`,
			encoderMD:    "Text",
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment"))`,
			encoderSexpr: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment"))`,
			encoderSHTML: `("Text" (@@ "comment"))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text and with -->",
		zmk:   `Text %%{-} comment --> end`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment --> end"}]`,
			encoderHTML:  `Text<!-- comment --&gt; end -->`,
			encoderHTML:  `Text<!-- comment -&#45;> end -->`,
			encoderMD:    "Text",
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment --> end"))`,
			encoderSexpr: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment --> end"))`,
			encoderSHTML: `("Text" (@@ "comment --> end"))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple footnote",
		zmk:   `[^footnote]`,
		descr: "Simple inline endnote",
		zmk:   `[^endnote]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Footnote","i":[{"":"Text","s":"footnote"}]}]`,
			encoderHTML:  `<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup>`,
			encoderMD:    "",
			encoderSexpr: `((FOOTNOTE () (TEXT "footnote")))`,
			encoderText:  `footnote`,
			encoderSexpr: `(INLINE (ENDNOTE () (quote (INLINE (TEXT "endnote")))))`,
			encoderSHTML: `((sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1")))`,
			encoderText:  `endnote`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple mark",
		zmk:   `[!mark]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Mark","s":"mark","q":"mark"}]`,
			encoderHTML:  `<a id="mark"></a>`,
			encoderMD:    "",
			encoderSexpr: `((MARK "mark" "mark" "mark"))`,
			encoderSexpr: `(INLINE (MARK "mark" "mark" "mark"))`,
			encoderSHTML: `((a (@ (id . "mark"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark with text",
		zmk:   `[!mark|with text]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Mark","s":"mark","q":"mark","i":[{"":"Text","s":"with"},{"":"Space"},{"":"Text","s":"text"}]}]`,
			encoderHTML:  `<a id="mark">with text</a>`,
			encoderMD:    "with text",
			encoderSexpr: `((MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`,
			encoderSexpr: `(INLINE (MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`,
			encoderSHTML: `((a (@ (id . "mark")) "with" " " "text"))`,
			encoderText:  `with text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Link",
		zmk:   `[[link|00000000000000]]`,
		expect: expectMap{
			encoderHTML:  `<span>link</span>`,
			encoderMD:    "[link](00000000000000)",
			encoderSexpr: `(INLINE (LINK-INVALID () "00000000000000" (TEXT "link")))`,
			encoderSHTML: `((span "link"))`,
			encoderText:  `link`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Simple Link",
		zmk:   `[[00000000000000]]`,
		expect: expectMap{
			encoderHTML:  `<span>00000000000000</span>`,
			encoderMD:    "[00000000000000](00000000000000)",
			encoderSexpr: `(INLINE (LINK-INVALID () "00000000000000"))`,
			encoderSHTML: `((span "00000000000000"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Link",
		zmk:   `[[abc]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"external","s":"abc"}]`,
			encoderHTML:  `<a class="external" href="abc">abc</a>`,
			encoderMD:    "[abc](abc)",
			encoderSexpr: `((LINK-EXTERNAL () "abc"))`,
			encoderSexpr: `(INLINE (LINK-EXTERNAL () "abc"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "abc")) "abc"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple URL",
		zmk:   `[[https://zettelstore.de]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"external","s":"https://zettelstore.de"}]`,
			encoderHTML:  `<a class="external" href="https://zettelstore.de">https://zettelstore.de</a>`,
			encoderMD:    "<https://zettelstore.de>",
			encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSexpr: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "https://zettelstore.de"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "URL with Text",
		zmk:   `[[Home|https://zettelstore.de]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"external","s":"https://zettelstore.de","i":[{"":"Text","s":"Home"}]}]`,
			encoderHTML:  `<a class="external" href="https://zettelstore.de">Home</a>`,
			encoderMD:    "[Home](https://zettelstore.de)",
			encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSexpr: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "Home"))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100"}]`,
			encoderHTML:  `<a href="00000000000100">00000000000100</a>`,
			encoderMD:    "[00000000000100](00000000000100)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100"))`,
			encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100"))`,
			encoderSHTML: `((a (@ (href . "00000000000100")) "00000000000100"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Zettel ID with Text",
		zmk:   `[[Config|00000000000100]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100","i":[{"":"Text","s":"Config"}]}]`,
			encoderHTML:  `<a href="00000000000100">Config</a>`,
			encoderMD:    "[Config](00000000000100)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100" (TEXT "Config")))`,
			encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100" (TEXT "Config")))`,
			encoderSHTML: `((a (@ (href . "00000000000100")) "Config"))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID with fragment",
		zmk:   `[[00000000000100#frag]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100#frag"}]`,
			encoderHTML:  `<a href="00000000000100#frag">00000000000100#frag</a>`,
			encoderMD:    "[00000000000100#frag](00000000000100#frag)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag"))`,
			encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100#frag"))`,
			encoderSHTML: `((a (@ (href . "00000000000100#frag")) "00000000000100#frag"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Zettel ID with Text and fragment",
		zmk:   `[[Config|00000000000100#frag]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100#frag","i":[{"":"Text","s":"Config"}]}]`,
			encoderHTML:  `<a href="00000000000100#frag">Config</a>`,
			encoderMD:    "[Config](00000000000100#frag)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`,
			encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`,
			encoderSHTML: `((a (@ (href . "00000000000100#frag")) "Config"))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Fragment link to self",
		zmk:   `[[#frag]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"self","s":"#frag"}]`,
			encoderHTML:  `<a href="#frag">#frag</a>`,
			encoderMD:    "[#frag](#frag)",
			encoderSexpr: `((LINK-SELF () "#frag"))`,
			encoderSexpr: `(INLINE (LINK-SELF () "#frag"))`,
			encoderSHTML: `((a (@ (href . "#frag")) "#frag"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Hosted link",
		zmk:   `[[H|/hosted]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"local","s":"/hosted","i":[{"":"Text","s":"H"}]}]`,
			encoderHTML:  `<a href="/hosted">H</a>`,
			encoderMD:    "[H](/hosted)",
			encoderSexpr: `((LINK-HOSTED () "/hosted" (TEXT "H")))`,
			encoderSexpr: `(INLINE (LINK-HOSTED () "/hosted" (TEXT "H")))`,
			encoderSHTML: `((a (@ (href . "/hosted")) "H"))`,
			encoderText:  `H`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Based link",
		zmk:   `[[B|/based]]`,
		zmk:   `[[B|//based]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"local","s":"/based","i":[{"":"Text","s":"B"}]}]`,
			encoderHTML:  `<a href="/based">B</a>`,
			encoderMD:    "[B](/based)",
			encoderSexpr: `((LINK-HOSTED () "/based" (TEXT "B")))`,
			encoderSexpr: `(INLINE (LINK-BASED () "/based" (TEXT "B")))`,
			encoderText:  `B`,
			encoderSHTML: `((a (@ (href . "/based")) "B"))`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Relative link",
		zmk:   `[[R|../relative]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"local","s":"../relative","i":[{"":"Text","s":"R"}]}]`,
			encoderHTML:  `<a href="../relative">R</a>`,
			encoderMD:    "[R](../relative)",
			encoderSexpr: `((LINK-HOSTED () "../relative" (TEXT "R")))`,
			encoderSexpr: `(INLINE (LINK-HOSTED () "../relative" (TEXT "R")))`,
			encoderSHTML: `((a (@ (href . "../relative")) "R"))`,
			encoderText:  `R`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Query link w/o text",
		zmk:   `[[query:title:syntax]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax"}]`,
			encoderHTML:  `<a href="?q=title%3Asyntax">title:syntax</a>`,
			encoderMD:    "",
			encoderSexpr: `((LINK-QUERY () "title:syntax"))`,
			encoderSexpr: `(INLINE (LINK-QUERY () "title:syntax"))`,
			encoderSHTML: `((a (@ (href . "?q=title%3Asyntax")) "title:syntax"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Query link with text",
		zmk:   `[[Q|query:title:syntax]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax","i":[{"":"Text","s":"Q"}]}]`,
			encoderHTML:  `<a href="?q=title%3Asyntax">Q</a>`,
			encoderMD:    "Q",
			encoderSexpr: `((LINK-QUERY () "title:syntax" (TEXT "Q")))`,
			encoderSexpr: `(INLINE (LINK-QUERY () "title:syntax" (TEXT "Q")))`,
			encoderSHTML: `((a (@ (href . "?q=title%3Asyntax")) "Q"))`,
			encoderText:  `Q`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Embed",
		zmk:   `{{abc}}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Embed","s":"abc"}]`,
			encoderHTML:  `<img alt="alternate description missing" src="abc">`,
			encoderHTML:  `<img src="abc">`,
			encoderMD:    "![abc](abc)",
			encoderSexpr: `((EMBED () (EXTERNAL "abc") ""))`,
			encoderSexpr: `(INLINE (EMBED () (quote (EXTERNAL "abc")) ""))`,
			encoderSHTML: `((img (@ (src . "abc"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Inline HTML Zettel",
		zmk:   `@@<hr>@@{="html"}`,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `()`,
			encoderSexpr: `(INLINE)`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   ``,
		},
	},
	{
		descr: "Inline Text Zettel",
		zmk:   `@@<hr>@@{="text"}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Zettel","a":{"":"text"},"s":"<hr>"}]`,
			encoderHTML:  ``,
			encoderMD:    "<hr>",
			encoderSexpr: `((LITERAL-ZETTEL (("" "text")) "<hr>"))`,
			encoderSexpr: `(INLINE (LITERAL-ZETTEL (quote (("" . "text"))) "<hr>"))`,
			encoderSHTML: `(())`,
			encoderText:  `<hr>`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `()`,
			encoderSexpr: `(INLINE)`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
}

Changes to encoder/encoder_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
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

-
+











-

+


-
+

-










+

-
















-



+







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

package encoder_test

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"codeberg.org/t73fde/sxpf"
	"codeberg.org/t73fde/sxpf/reader"
	"zettelstore.de/c/api"
	"zettelstore.de/c/sexpr"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"

	_ "zettelstore.de/z/encoder/htmlenc"  // Allow to use HTML encoder.
	_ "zettelstore.de/z/encoder/mdenc"    // Allow to use markdown encoder.
	_ "zettelstore.de/z/encoder/sexprenc" // Allow to use sexpr encoder.
	_ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder.
	_ "zettelstore.de/z/encoder/textenc"  // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zjsonenc" // Allow to use ZJSON encoder.
	_ "zettelstore.de/z/encoder/zmkenc"   // Allow to use zmk encoder.
	"zettelstore.de/z/parser/cleaner"
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

type zmkTestCase struct {
	descr  string
	zmk    string
	inline bool
	expect expectMap
}

type expectMap map[api.EncodingEnum]string

const useZmk = "\000"
const (
	encoderZJSON = api.EncoderZJSON
	encoderHTML  = api.EncoderHTML
	encoderMD    = api.EncoderMD
	encoderSexpr = api.EncoderSexpr
	encoderSHTML = api.EncoderSHTML
	encoderText  = api.EncoderText
	encoderZmk   = api.EncoderZmk
)

func TestEncoder(t *testing.T) {
	for i := range tcsInline {
		tcsInline[i].inline = true
78
79
80
81
82
83
84





85

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

89
90
91
92
93
94
95
96







+
+
+
+
+
-
+







}

func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) {
	for enc, exp := range expected {
		encdr := encoder.Create(enc)
		got, err := pe.encode(encdr)
		if err != nil {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + pe.mode()
			t.Error("pe.encode:", err)
			t.Errorf("%s\nEncoder:  %s\nError:    %v", prefix, enc, err)
			continue
		}
		if enc == api.EncoderZmk && exp == useZmk {
			exp = zmkDefault
		}
		if got != exp {
			prefix := fmt.Sprintf("Test #%d", testNum)
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
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







-
+




-
+
-
-
-
-




















-
-
+
+


-
+









-
-
+
+


-
+



	t.Helper()
	encdr := encoder.Create(encoderSexpr)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxpf.ParseString(sexpr.Smk, exp)
	val, err := reader.MakeReader(strings.NewReader(exp)).Read()
	if err != nil {
		t.Error(err)
		return
	}
	got, err := sxpf.Repr(val)
	got := val.Repr()
	if err != nil {
		t.Error(err)
		return
	}
	if exp != got {
		prefix := fmt.Sprintf("Test #%d", testNum)
		if d := descr; d != "" {
			prefix += "\nReason:   " + d
		}
		prefix += "\nMode:     " + pe.mode()
		t.Errorf("%s\n\nExpected: %q\nGot:      %q", prefix, exp, got)
	}
}

type parserEncoder interface {
	encode(encoder.Encoder) (string, error)
	mode() string
}

type peInlines struct {
	is ast.InlineSlice
}

func (in peInlines) encode(encdr encoder.Encoder) (string, error) {
	var buf bytes.Buffer
	if _, err := encdr.WriteInlines(&buf, &in.is); err != nil {
	var sb strings.Builder
	if _, err := encdr.WriteInlines(&sb, &in.is); err != nil {
		return "", err
	}
	return buf.String(), nil
	return sb.String(), nil
}

func (peInlines) mode() string { return "inline" }

type peBlocks struct {
	bs ast.BlockSlice
}

func (bl peBlocks) encode(encdr encoder.Encoder) (string, error) {
	var buf bytes.Buffer
	if _, err := encdr.WriteBlocks(&buf, &bl.bs); err != nil {
	var sb strings.Builder
	if _, err := encdr.WriteBlocks(&sb, &bl.bs); err != nil {
		return "", err
	}
	return buf.String(), nil
	return sb.String(), nil

}
func (peBlocks) mode() string { return "block" }

Changes to encoder/htmlenc/htmlenc.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
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

-
+















+


-
+




+

+



















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


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

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

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




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








+
+
-
-
+
+
+

+
-
-
+
+
+
+
+
+
+
+






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

//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

// Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client.
package htmlenc

import (
	"io"
	"strings"

	"codeberg.org/t73fde/sxhtml"
	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/c/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/sexprenc"
	"zettelstore.de/z/encoder/shtmlenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
)

func init() {
	encoder.Register(api.EncoderHTML, func() encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &mySHE }

type Encoder struct {
	textEnc *textenc.Encoder
}

var mySHE = Encoder{
	textEnc: textenc.Create(),
}

// WriteZettel encodes a full zettel as HTML5.
func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	tx := sexprenc.NewTransformer()
	xm := tx.GetMeta(zn.InhMeta, evalMeta)
	io.WriteString(w, "<html>\n<head>\n<meta charset=\"utf-8\">\n")

	th := shtml.NewTransformer(1, nil)
	hm, err := th.Transform(xm)
	if err != nil {
		return 0, err
	}

	var isTitle ast.InlineSlice
	var htitle *sxpf.List
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		io.WriteString(w, "<title>")
		is := evalMeta(plainTitle)
		he.textEnc.WriteInlines(w, &is)
		io.WriteString(w, "</title>\n")
	}

		isTitle = parser.ParseSpacedText(plainTitle)
		xtitle := tx.GetSexpr(&isTitle)
		htitle, err = th.Transform(xtitle)
		if err != nil {
			return 0, err
		}
	}

	xast := tx.GetSexpr(&zn.Ast)
	hast, err := th.Transform(xast)
	if err != nil {
		return 0, err
	}
	hen := th.Endnotes()

	sf := th.SymbolFactory()
	symAttr := sf.MustMake(sxhtml.NameSymAttr)

	head := sxpf.Nil().Cons(sf.MustMake("head"))
	curr := head
	curr = curr.AppendBang(sxpf.Nil().Cons(sxpf.Nil().Cons(sxpf.Cons(sf.MustMake("charset"), sxpf.MakeString("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta")))
	for elem := hm; elem != nil; elem = elem.Tail() {
		curr = curr.AppendBang(elem.Car())
	}
	acceptMeta(w, he.textEnc, zn.InhMeta, evalMeta)
	io.WriteString(w, "</head>\n<body>\n")
	env := html.NewEncEnvironment(w, 1)
	if hasTitle {
		if isTitle := evalMeta(plainTitle); len(isTitle) > 0 {
			io.WriteString(w, "<h1>")
			if err := acceptInlines(env, &isTitle); err != nil {
		var sb strings.Builder
		he.textEnc.WriteInlines(&sb, &isTitle)
				return 0, err
			}
		_ = curr.AppendBang(sxpf.Nil().Cons(sxpf.MakeString(sb.String())).Cons(sf.MustMake("title")))
	}
			io.WriteString(w, "</h1>\n")
		}

	body := sxpf.Nil().Cons(sf.MustMake("body"))
	curr = body
	if hasTitle {
		curr = curr.AppendBang(htitle.Cons(sf.MustMake("h1")))
	}
	for elem := hast; elem != nil; elem = elem.Tail() {
		curr = curr.AppendBang(elem.Car())

	}
	err := acceptBlocks(env, &zn.Ast)
	if err == nil {
		// env.WriteEndnotes()
		io.WriteString(w, "</body>\n</html>")
	}
	return 0, err
	if hen != nil {
		curr = curr.AppendBang(sxpf.Nil().Cons(sf.MustMake("hr")))
		_ = curr.AppendBang(hen)
	}

	doc := sxpf.MakeList(
		sf.MustMake(sxhtml.NameSymDoctype),
		sxpf.MakeList(sf.MustMake("html"), head, body),
	)

	gen := sxhtml.NewGenerator(sf, sxhtml.WithNewline)
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	tx := sexprenc.NewTransformer()
	acceptMeta(w, he.textEnc, m, evalMeta)
	return 0, nil
	xm := tx.GetMeta(m, evalMeta)

	th := shtml.NewTransformer(1, nil)
	hm, err := th.Transform(xm)
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator(th.SymbolFactory(), sxhtml.WithNewline)
	return gen.WriteListHTML(w, hm)
}

func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks encodes a block slice.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	tx := sexprenc.NewTransformer()
	xval := tx.GetSexpr(bs)
	env := html.NewEncEnvironment(w, 1)
	err := acceptBlocks(env, bs)
	th := shtml.NewTransformer(1, nil)
	hobj, err := th.Transform(xval)

	if err == nil {
		gen := sxhtml.NewGenerator(th.SymbolFactory())
		env.WriteEndnotes()
		err = env.GetError()
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, th.Endnotes())
		length += l
		return length, err2
	}
	return 0, err
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := html.NewEncEnvironment(w, 1)
	return 0, acceptInlines(env, is)
	hobj, err := shtmlenc.TransformSlice(is)
	if err == nil {
}

		gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(hobj))
func acceptMeta(w io.Writer, textEnc encoder.Encoder, m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
		io.WriteString(w, `<meta name="zs-`)
		io.WriteString(w, p.Key)
		length, err2 := gen.WriteListHTML(w, hobj)
		io.WriteString(w, `" content="`)
		is := evalMeta(p.Value)
		var sb strings.Builder
		if err2 != nil {
			return length, err2
		textEnc.WriteInlines(&sb, &is)
		html.AttributeEscape(w, sb.String())
		io.WriteString(w, "\">\n")
	}
		}
}

func acceptBlocks(env *html.EncEnvironment, bs *ast.BlockSlice) error {
	lst := sexprenc.GetSexpr(bs)
	sxpf.Eval(env, lst)
	return env.GetError()
}
		return length, nil
	}
func acceptInlines(env *html.EncEnvironment, is *ast.InlineSlice) error {
	lst := sexprenc.GetSexpr(is)
	sxpf.Eval(env, lst)
	return env.GetError()
	return 0, err
}

Changes to encoder/mdenc/mdenc.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to encoder/sexprenc/sexprenc.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
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







+
-
-
-
+
+
+


-
+

+
-
+








+
-
+




+
-
+


type Encoder struct{}

var mySE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	t := NewTransformer()
	content := GetSexpr(&zn.Ast)
	meta := GetMeta(zn.InhMeta, evalMeta)
	return io.WriteString(w, sxpf.NewPair(meta, sxpf.NewPair(content, nil)).String())
	content := t.GetSexpr(&zn.Ast)
	meta := t.GetMeta(zn.InhMeta, evalMeta)
	return io.WriteString(w, sxpf.Nil().Cons(content).Cons(meta).Repr())
}

// WriteMeta encodes meta data as JSON.
// WriteMeta encodes meta data as s-expression.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	t := NewTransformer()
	return io.WriteString(w, GetMeta(m, evalMeta).String())
	return io.WriteString(w, t.GetMeta(m, evalMeta).Repr())
}

func (se *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return se.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	t := NewTransformer()
	return io.WriteString(w, GetSexpr(bs).String())
	return io.WriteString(w, t.GetSexpr(bs).Repr())
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	t := NewTransformer()
	return io.WriteString(w, GetSexpr(is).String())
	return io.WriteString(w, t.GetSexpr(is).Repr())
}

Changes to encoder/sexprenc/transform.go.

1
2

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

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























































































31
32
33
34













35
36
37

38
39
40
41
42
43
44

45
46
47
48
49




50
51
52
53
54
55


56
57

58
59

60
61

62
63
64


65
66
67
68
69
70
71

72
73
74
75
76
77
78
79

80
81
82
83

84
85
86

87
88

89
90
91

92
93
94

95
96
97
98

99
100

101
102
103

104
105
106

107
108
109
110

111
112
113
114

115
116
117


118
119
120
121
122
123

124
125
126
127
128


129
130
131
132
133

134
135
136

137
138

139
140

141
142
143
144
145

146
147

148
149
150

151
152
153

154
155
156
157
158




159
160
161
162

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

181
182
183
184
185

186
187
188
189



190
191

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



204
205
206
207
208


209
210
211

212
213

214

215
216
217






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



236
237
238


239
240
241
242
243
244

245
246

247
248

249
250
251
252
253
254

255
256
257
258
259
260
261
262

263
264
265
266
267







268
269

270
271

272
273
274
275
276
277
278

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

291
292
293




294
295
296
297
298







299
300
301
302
303


304
305
306
307
308
309
310
311
312









313
314
315
316
317
318





319
320






321
322
323
324


325
326

327
328

329
330
331
332
333

334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351

352
353
354
355
356
357
358
359
360
361
362
363
364


365
366

367
368

369
370
371


372
373

374
375

376
377
378

379
380
381
382
383

384
385

386
387
388
389

390
391
392
393
394
395
396
397
398
399
400
401
402
403
404





405
406
407


408
409

410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425

426
427

428
429
430
431
432
433



434
435
436

437
438

439
440

441
442
443
444

445
446

447
448

449
450

451
452
453

454
455
456
457
458

459
460
461
462
463



464
465
466
467
468
469

470
471

472
1

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

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




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


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

131
132
133
134
135
136
137

138
139




140
141
142
143
144
145
146
147


148
149


150


151


152



153
154




155
156

157
158
159
160
161
162
163
164

165
166
167
168

169
170
171

172
173

174
175
176

177


178
179
180
181
182

183


184



185



186




187
188
189
190

191



192
193






194
195




196
197



198

199



200


201


202





203
204

205



206



207
208




209
210
211
212
213
214
215

216
217
218
















219
220
221
222
223

224
225



226
227
228
229

230
231
232
233









234
235
236
237
238
239


240
241
242
243

244
245

246
247
248



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269



270
271
272
273


274
275
276





277
278

279
280

281
282





283






284

285
286




287
288
289
290
291
292
293
294

295
296

297







298












299



300
301
302
303





304
305
306
307
308
309
310





311
312




313
314



315
316
317
318
319
320
321
322
323






324
325
326
327
328


329
330
331
332
333
334
335
336


337
338
339

340
341

342
343




344






345











346













347
348
349

350
351

352
353


354
355
356

357
358

359
360
361

362
363
364
365
366

367
368

369
370



371










372
373



374
375
376
377
378



379
380


381













382
383

384
385

386
387
388
389



390
391
392
393
394

395
396

397
398

399
400
401


402
403

404
405

406
407

408
409
410

411
412
413
414
415

416
417
418



419
420
421
422
423
424
425
426

427
428

429
430

-
+











-



+









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


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


-
+






-
+

-
-
-
-
+
+
+
+




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


-
+







-
+



-
+


-
+

-
+


-
+
-
-

+



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



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

-
-
-
-
+
+
-
-
-

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

-
+
-
-
-
+
-
-
-
+

-
-
-
-
+
+
+
+



-
+


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




-
+

-
-
-
+
+
+

-
+



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



-
-
+
+


-
+

-
+

+
-
-
-
+
+
+
+
+
+















-
-
-
+
+
+

-
-
+
+

-
-
-
-
-
+

-
+

-
+

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

-
+

-
-
-
-
+
+
+
+
+
+
+

-
+

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


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


-
-
+
+

-
+

-
+

-
-
-
-
+
-
-
-
-
-
-

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

-
+

-
+

-
-
+
+

-
+

-
+


-
+




-
+

-
+

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


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


-
+

-
+



-
-
-
+
+
+


-
+

-
+

-
+


-
-
+

-
+

-
+

-
+


-
+




-
+


-
-
-
+
+
+





-
+

-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

package sexprenc

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"log"
	"strings"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/sexpr"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

// GetSexpr returns the given node as a s-expression.
func GetSexpr(node ast.Node) *sxpf.Pair {
	t := transformer{}
	return t.getSexpr(node)
// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	sf := sxpf.MakeMappedFactory()
	t := Transformer{sf: sf}
	t.zetSyms.InitializeZettelSymbols(sf)

	t.mapVerbatimKindS = map[ast.VerbatimKind]*sxpf.Symbol{
		ast.VerbatimZettel:  t.zetSyms.SymVerbatimZettel,
		ast.VerbatimProg:    t.zetSyms.SymVerbatimProg,
		ast.VerbatimEval:    t.zetSyms.SymVerbatimEval,
		ast.VerbatimMath:    t.zetSyms.SymVerbatimMath,
		ast.VerbatimComment: t.zetSyms.SymVerbatimComment,
		ast.VerbatimHTML:    t.zetSyms.SymVerbatimHTML,
	}

	t.mapRegionKindS = map[ast.RegionKind]*sxpf.Symbol{
		ast.RegionSpan:  t.zetSyms.SymRegionBlock,
		ast.RegionQuote: t.zetSyms.SymRegionQuote,
		ast.RegionVerse: t.zetSyms.SymRegionVerse,
	}
	t.mapNestedListKindS = map[ast.NestedListKind]*sxpf.Symbol{
		ast.NestedListOrdered:   t.zetSyms.SymListOrdered,
		ast.NestedListUnordered: t.zetSyms.SymListUnordered,
		ast.NestedListQuote:     t.zetSyms.SymListQuote,
	}
	t.alignmentSymbolS = map[ast.Alignment]*sxpf.Symbol{
		ast.AlignDefault: t.zetSyms.SymCell,
		ast.AlignLeft:    t.zetSyms.SymCellLeft,
		ast.AlignCenter:  t.zetSyms.SymCellCenter,
		ast.AlignRight:   t.zetSyms.SymCellRight,
	}
	t.mapRefStateLink = map[ast.RefState]*sxpf.Symbol{
		ast.RefStateInvalid:  t.zetSyms.SymLinkInvalid,
		ast.RefStateZettel:   t.zetSyms.SymLinkZettel,
		ast.RefStateSelf:     t.zetSyms.SymLinkSelf,
		ast.RefStateFound:    t.zetSyms.SymLinkFound,
		ast.RefStateBroken:   t.zetSyms.SymLinkBroken,
		ast.RefStateHosted:   t.zetSyms.SymLinkHosted,
		ast.RefStateBased:    t.zetSyms.SymLinkBased,
		ast.RefStateQuery:    t.zetSyms.SymLinkQuery,
		ast.RefStateExternal: t.zetSyms.SymLinkExternal,
	}
	t.mapFormatKindS = map[ast.FormatKind]*sxpf.Symbol{
		ast.FormatEmph:   t.zetSyms.SymFormatEmph,
		ast.FormatStrong: t.zetSyms.SymFormatStrong,
		ast.FormatDelete: t.zetSyms.SymFormatDelete,
		ast.FormatInsert: t.zetSyms.SymFormatInsert,
		ast.FormatSuper:  t.zetSyms.SymFormatSuper,
		ast.FormatSub:    t.zetSyms.SymFormatSub,
		ast.FormatQuote:  t.zetSyms.SymFormatQuote,
		ast.FormatSpan:   t.zetSyms.SymFormatSpan,
	}
	t.mapLiteralKindS = map[ast.LiteralKind]*sxpf.Symbol{
		ast.LiteralZettel:  t.zetSyms.SymLiteralZettel,
		ast.LiteralProg:    t.zetSyms.SymLiteralProg,
		ast.LiteralInput:   t.zetSyms.SymLiteralInput,
		ast.LiteralOutput:  t.zetSyms.SymLiteralOutput,
		ast.LiteralComment: t.zetSyms.SymLiteralComment,
		ast.LiteralHTML:    t.zetSyms.SymLiteralHTML,
		ast.LiteralMath:    t.zetSyms.SymLiteralMath,
	}
	t.mapRefStateS = map[ast.RefState]*sxpf.Symbol{
		ast.RefStateInvalid:  t.zetSyms.SymRefStateInvalid,
		ast.RefStateZettel:   t.zetSyms.SymRefStateZettel,
		ast.RefStateSelf:     t.zetSyms.SymRefStateSelf,
		ast.RefStateFound:    t.zetSyms.SymRefStateFound,
		ast.RefStateBroken:   t.zetSyms.SymRefStateBroken,
		ast.RefStateHosted:   t.zetSyms.SymRefStateHosted,
		ast.RefStateBased:    t.zetSyms.SymRefStateBased,
		ast.RefStateQuery:    t.zetSyms.SymRefStateQuery,
		ast.RefStateExternal: t.zetSyms.SymRefStateExternal,
	}
	t.mapMetaTypeS = map[*meta.DescriptionType]*sxpf.Symbol{
		meta.TypeCredential:   t.zetSyms.SymTypeCredential,
		meta.TypeEmpty:        t.zetSyms.SymTypeEmpty,
		meta.TypeID:           t.zetSyms.SymTypeID,
		meta.TypeIDSet:        t.zetSyms.SymTypeIDSet,
		meta.TypeNumber:       t.zetSyms.SymTypeNumber,
		meta.TypeString:       t.zetSyms.SymTypeString,
		meta.TypeTagSet:       t.zetSyms.SymTypeTagSet,
		meta.TypeTimestamp:    t.zetSyms.SymTypeTimestamp,
		meta.TypeURL:          t.zetSyms.SymTypeURL,
		meta.TypeWord:         t.zetSyms.SymTypeWord,
		meta.TypeWordSet:      t.zetSyms.SymTypeWordSet,
		meta.TypeZettelmarkup: t.zetSyms.SymTypeZettelmarkup,
	}
	return &t
}

type transformer struct {
	inVerse bool
type Transformer struct {
	sf                 sxpf.SymbolFactory
	zetSyms            sexpr.ZettelSymbols
	mapVerbatimKindS   map[ast.VerbatimKind]*sxpf.Symbol
	mapRegionKindS     map[ast.RegionKind]*sxpf.Symbol
	mapNestedListKindS map[ast.NestedListKind]*sxpf.Symbol
	alignmentSymbolS   map[ast.Alignment]*sxpf.Symbol
	mapRefStateLink    map[ast.RefState]*sxpf.Symbol
	mapFormatKindS     map[ast.FormatKind]*sxpf.Symbol
	mapLiteralKindS    map[ast.LiteralKind]*sxpf.Symbol
	mapRefStateS       map[ast.RefState]*sxpf.Symbol
	mapMetaTypeS       map[*meta.DescriptionType]*sxpf.Symbol
	inVerse            bool
}

func (t *transformer) getSexpr(node ast.Node) *sxpf.Pair {
func (t *Transformer) GetSexpr(node ast.Node) *sxpf.List {
	switch n := node.(type) {
	case *ast.BlockSlice:
		return t.getBlockSlice(n)
	case *ast.InlineSlice:
		return t.getInlineSlice(*n)
	case *ast.ParaNode:
		return sxpf.NewPair(sexpr.SymPara, t.getInlineSlice(n.Inlines))
		return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara)
	case *ast.VerbatimNode:
		return sxpf.NewPairFromValues(
			mapGetS(mapVerbatimKindS, n.Kind),
			getAttributes(n.Attrs),
			sxpf.NewString(string(n.Content)),
		return sxpf.MakeList(
			mapGetS(t, t.mapVerbatimKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sxpf.MakeString(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return sxpf.NewPair(
			sexpr.SymHeading,
		return sxpf.MakeList(
			t.zetSyms.SymHeading,
			sxpf.NewPair(
				sxpf.NewInteger(int64(n.Level)),
			sxpf.MakeInteger64(int64(n.Level)),
				sxpf.NewPair(
					getAttributes(n.Attrs),
			t.getAttributes(n.Attrs),
					sxpf.NewPair(
						sxpf.NewString(n.Slug),
			sxpf.MakeString(n.Slug),
						sxpf.NewPair(
							sxpf.NewString(n.Fragment),
							t.getInlineSlice(n.Inlines),
			sxpf.MakeString(n.Fragment),
			t.getInlineSlice(n.Inlines),
						),
					),
				),
			),
		)
	case *ast.HRuleNode:
		return sxpf.NewPairFromValues(sexpr.SymThematic, getAttributes(n.Attrs))
		return sxpf.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return sxpf.NewPairFromValues(sexpr.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref))
		return sxpf.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sxpf.NewPairFromValues(sexpr.SymText, sxpf.NewString(n.Text))
		return sxpf.MakeList(t.zetSyms.SymText, sxpf.MakeString(n.Text))
	case *ast.SpaceNode:
		if t.inVerse {
			return sxpf.NewPairFromValues(sexpr.SymSpace, sxpf.NewString(n.Lexeme))
			return sxpf.MakeList(t.zetSyms.SymSpace, sxpf.MakeString(n.Lexeme))
		}
		return sxpf.NewPairFromValues(sexpr.SymSpace)
		return sxpf.MakeList(t.zetSyms.SymSpace)
	case *ast.BreakNode:
		if n.Hard {
			return sxpf.NewPairFromValues(sexpr.SymHard)
			return sxpf.MakeList(t.zetSyms.SymHard)
		} else {
			return sxpf.NewPairFromValues(sexpr.SymSoft)
		}
		return sxpf.MakeList(t.zetSyms.SymSoft)
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return sxpf.NewPair(
		return t.getInlineSlice(n.Inlines).Tail().
			sexpr.SymEmbed,
			sxpf.NewPair(
			Cons(sxpf.MakeString(n.Syntax)).
				getAttributes(n.Attrs),
				sxpf.NewPair(
					getReference(n.Ref),
			Cons(t.getReference(n.Ref)).
					sxpf.NewPair(
						sxpf.NewString(n.Syntax),
						t.getInlineSlice(n.Inlines),
			Cons(t.getAttributes(n.Attrs)).
					),
				),
			),
		)
			Cons(t.zetSyms.SymEmbed)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return sxpf.NewPair(
		return t.getInlineSlice(n.Inlines).Tail().
			sexpr.SymCite,
			sxpf.NewPair(
				getAttributes(n.Attrs),
			Cons(sxpf.MakeString(n.Key)).
			Cons(t.getAttributes(n.Attrs)).
				sxpf.NewPair(
					sxpf.NewString(n.Key),
					t.getInlineSlice(n.Inlines),
				),
			),
		)
			Cons(t.zetSyms.SymCite)
	case *ast.FootnoteNode:
		return sxpf.NewPair(
			sexpr.SymFootnote,
			sxpf.NewPair(
				getAttributes(n.Attrs),
		text := sxpf.Nil().Cons(sxpf.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote))
		return text.Cons(t.getAttributes(n.Attrs)).Cons(t.zetSyms.SymEndnote)
				t.getInlineSlice(n.Inlines),
			),
		)
	case *ast.MarkNode:
		return sxpf.NewPair(
		return t.getInlineSlice(n.Inlines).Tail().
			sexpr.SymMark,
			sxpf.NewPair(
				sxpf.NewString(n.Mark),
			Cons(sxpf.MakeString(n.Fragment)).
				sxpf.NewPair(
					sxpf.NewString(n.Slug),
			Cons(sxpf.MakeString(n.Slug)).
					sxpf.NewPair(
						sxpf.NewString(n.Fragment),
			Cons(sxpf.MakeString(n.Mark)).
						t.getInlineSlice(n.Inlines),
					),
				),
			),
		)
			Cons(t.zetSyms.SymMark)
	case *ast.FormatNode:
		return sxpf.NewPair(
		return t.getInlineSlice(n.Inlines).Tail().
			mapGetS(mapFormatKindS, n.Kind),
			sxpf.NewPair(
				getAttributes(n.Attrs),
			Cons(t.getAttributes(n.Attrs)).
				t.getInlineSlice(n.Inlines),
			),
		)
			Cons(mapGetS(t, t.mapFormatKindS, n.Kind))
	case *ast.LiteralNode:
		return sxpf.NewPairFromValues(
			mapGetS(mapLiteralKindS, n.Kind),
			getAttributes(n.Attrs),
			sxpf.NewString(string(n.Content)),
		return sxpf.MakeList(
			mapGetS(t, t.mapLiteralKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sxpf.MakeString(string(n.Content)),
		)
	}
	log.Printf("SEXPR %T %v\n", node, node)
	return sxpf.NewPairFromValues(sexpr.SymUnknown, sxpf.NewString(fmt.Sprintf("%T %v", node, node)))
	return sxpf.MakeList(t.zetSyms.SymUnknown, sxpf.MakeString(fmt.Sprintf("%T %v", node, node)))
}

var mapVerbatimKindS = map[ast.VerbatimKind]*sxpf.Symbol{
	ast.VerbatimZettel:  sexpr.SymVerbatimZettel,
	ast.VerbatimProg:    sexpr.SymVerbatimProg,
	ast.VerbatimEval:    sexpr.SymVerbatimEval,
	ast.VerbatimMath:    sexpr.SymVerbatimMath,
	ast.VerbatimComment: sexpr.SymVerbatimComment,
	ast.VerbatimHTML:    sexpr.SymVerbatimHTML,
}

var mapRegionKindS = map[ast.RegionKind]*sxpf.Symbol{
	ast.RegionSpan:  sexpr.SymRegionBlock,
	ast.RegionQuote: sexpr.SymRegionQuote,
	ast.RegionVerse: sexpr.SymRegionVerse,
}

func (t *transformer) getRegion(rn *ast.RegionNode) *sxpf.Pair {
func (t *Transformer) getRegion(rn *ast.RegionNode) *sxpf.List {
	saveInVerse := t.inVerse
	if rn.Kind == ast.RegionVerse {
		t.inVerse = true
	}
	symBlocks := t.getSexpr(&rn.Blocks)
	symBlocks := t.GetSexpr(&rn.Blocks)
	t.inVerse = saveInVerse
	return sxpf.NewPairFromValues(
		mapGetS(mapRegionKindS, rn.Kind),
		getAttributes(rn.Attrs),
	return sxpf.MakeList(
		mapGetS(t, t.mapRegionKindS, rn.Kind),
		t.getAttributes(rn.Attrs),
		symBlocks,
		t.getSexpr(&rn.Inlines),
		t.GetSexpr(&rn.Inlines),
	)
}

var mapNestedListKindS = map[ast.NestedListKind]*sxpf.Symbol{
	ast.NestedListOrdered:   sexpr.SymListOrdered,
	ast.NestedListUnordered: sexpr.SymListUnordered,
	ast.NestedListQuote:     sexpr.SymListQuote,
}

func (t *transformer) getNestedList(ln *ast.NestedListNode) *sxpf.Pair {
	nlistVals := make([]sxpf.Value, len(ln.Items)+1)
	nlistVals[0] = mapGetS(mapNestedListKindS, ln.Kind)
func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sxpf.List {
	nlistObjs := make([]sxpf.Object, len(ln.Items)+1)
	nlistObjs[0] = mapGetS(t, t.mapNestedListKindS, ln.Kind)
	isCompact := isCompactList(ln.Items)
	for i, item := range ln.Items {
		if isCompact && len(item) > 0 {
			paragraph := t.getSexpr(item[0])
			nlistVals[i+1] = paragraph.GetTail()
			paragraph := t.GetSexpr(item[0])
			nlistObjs[i+1] = paragraph.Tail().Cons(t.zetSyms.SymInline)
			continue
		}
		itemVals := make([]sxpf.Value, len(item))
		itemObjs := make([]sxpf.Object, len(item))
		for j, in := range item {
			itemVals[j] = t.getSexpr(in)
			itemObjs[j] = t.GetSexpr(in)
		}
		if isCompact {
		nlistVals[i+1] = sxpf.NewPairFromValues(itemVals...)
	}
	return sxpf.NewPairFromValues(nlistVals...)
			nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymInline)
		} else {
			nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymBlock)
		}
	}
	return sxpf.MakeList(nlistObjs...)
}
func isCompactList(itemSlice []ast.ItemSlice) bool {
	for _, items := range itemSlice {
		if len(items) > 1 {
			return false
		}
		if len(items) == 1 {
			if _, ok := items[0].(*ast.ParaNode); !ok {
				return false
			}
		}
	}
	return true
}

func (t *transformer) getDescriptionList(dn *ast.DescriptionListNode) *sxpf.Pair {
	dlVals := make([]sxpf.Value, 2*len(dn.Descriptions)+1)
	dlVals[0] = sexpr.SymDescription
func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sxpf.List {
	dlObjs := make([]sxpf.Object, 2*len(dn.Descriptions)+1)
	dlObjs[0] = t.zetSyms.SymDescription
	for i, def := range dn.Descriptions {
		dlVals[2*i+1] = t.getInlineSlice(def.Term)
		descVals := make([]sxpf.Value, len(def.Descriptions))
		dlObjs[2*i+1] = t.getInlineSlice(def.Term)
		descObjs := make([]sxpf.Object, len(def.Descriptions))
		for j, b := range def.Descriptions {
			if len(b) == 1 {
				descVals[j] = t.getSexpr(b[0]).GetTail()
				continue
			}
			dVal := make([]sxpf.Value, len(b))
			dVal := make([]sxpf.Object, len(b))
			for k, dn := range b {
				dVal[k] = t.getSexpr(dn)
				dVal[k] = t.GetSexpr(dn)
			}
			descVals[j] = sxpf.NewPairFromValues(dVal...)
			descObjs[j] = sxpf.MakeList(dVal...).Cons(t.zetSyms.SymBlock)
		}
		dlVals[2*i+2] = sxpf.NewPairFromValues(descVals...)
	}
	return sxpf.NewPairFromValues(dlVals...)
}

		dlObjs[2*i+2] = sxpf.MakeList(descObjs...).Cons(t.zetSyms.SymBlock)
func (t *transformer) getTable(tn *ast.TableNode) *sxpf.Pair {
	tVals := make([]sxpf.Value, len(tn.Rows)+2)
	tVals[0] = sexpr.SymTable
	tVals[1] = t.getRow(tn.Header)
	for i, row := range tn.Rows {
		tVals[i+2] = t.getRow(row)
	}
	return sxpf.NewPairFromValues(tVals...)
	return sxpf.MakeList(dlObjs...)
}
func (t *transformer) getRow(row ast.TableRow) *sxpf.Pair {
	rVals := make([]sxpf.Value, len(row))
	for i, cell := range row {
		rVals[i] = t.getCell(cell)

func (t *Transformer) getTable(tn *ast.TableNode) *sxpf.List {
	tObjs := make([]sxpf.Object, len(tn.Rows)+2)
	tObjs[0] = t.zetSyms.SymTable
	tObjs[1] = t.getHeader(tn.Header)
	for i, row := range tn.Rows {
		tObjs[i+2] = t.getRow(row)
	}
	return sxpf.NewPairFromValues(rVals...)
	return sxpf.MakeList(tObjs...)
}

func (t *Transformer) getHeader(header ast.TableRow) *sxpf.List {
var alignmentSymbolS = map[ast.Alignment]*sxpf.Symbol{
	ast.AlignDefault: sexpr.SymCell,
	ast.AlignLeft:    sexpr.SymCellLeft,
	ast.AlignCenter:  sexpr.SymCellCenter,
	ast.AlignRight:   sexpr.SymCellRight,
}

	if len(header) == 0 {
func (t *transformer) getCell(cell *ast.TableCell) *sxpf.Pair {
	return sxpf.NewPair(mapGetS(alignmentSymbolS, cell.Align), t.getInlineSlice(cell.Inlines))
}

func (t *transformer) getBLOB(bn *ast.BLOBNode) *sxpf.Pair {
	var lastValue sxpf.Value
	if bn.Syntax == meta.SyntaxSVG {
		lastValue = sxpf.NewString(string(bn.Blob))
	} else {
		lastValue = getBase64String(bn.Blob)
	}
	return sxpf.NewPairFromValues(
		return sxpf.Nil()
		sexpr.SymBLOB,
		t.getInlineSlice(bn.Description),
		sxpf.NewString(bn.Syntax),
	}
	return t.getRow(header)
}
func (t *Transformer) getRow(row ast.TableRow) *sxpf.List {
		lastValue,
	)
}

var mapRefStateLink = map[ast.RefState]*sxpf.Symbol{
	rObjs := make([]sxpf.Object, len(row))
	for i, cell := range row {
		rObjs[i] = t.getCell(cell)
	}
	return sxpf.MakeList(rObjs...).Cons(t.zetSyms.SymList)
}

	ast.RefStateInvalid:  sexpr.SymLinkInvalid,
	ast.RefStateZettel:   sexpr.SymLinkZettel,
	ast.RefStateSelf:     sexpr.SymLinkSelf,
	ast.RefStateFound:    sexpr.SymLinkFound,
	ast.RefStateBroken:   sexpr.SymLinkBroken,
func (t *Transformer) getCell(cell *ast.TableCell) *sxpf.List {
	return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align))
	ast.RefStateHosted:   sexpr.SymLinkHosted,
	ast.RefStateBased:    sexpr.SymLinkBased,
	ast.RefStateQuery:    sexpr.SymLinkQuery,
	ast.RefStateExternal: sexpr.SymLinkExternal,
}

func (t *transformer) getLink(ln *ast.LinkNode) *sxpf.Pair {
	return sxpf.NewPair(
		mapGetS(mapRefStateLink, ln.Ref.State),
func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sxpf.List {
	var lastObj sxpf.Object
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sxpf.MakeString(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sxpf.MakeList(
		t.zetSyms.SymBLOB,
		sxpf.NewPair(
			getAttributes(ln.Attrs),
			sxpf.NewPair(
				sxpf.NewString(ln.Ref.Value),
				t.getInlineSlice(ln.Inlines),
			),
		t.getInlineSlice(bn.Description),
		sxpf.MakeString(bn.Syntax),
		lastObj,
	)
}
		),
	)

func (t *Transformer) getLink(ln *ast.LinkNode) *sxpf.List {
	return t.getInlineSlice(ln.Inlines).Tail().
		Cons(sxpf.MakeString(ln.Ref.Value)).
		Cons(t.getAttributes(ln.Attrs)).
		Cons(mapGetS(t, t.mapRefStateLink, ln.Ref.State))
}

func (t *transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.Pair {
	tail := t.getInlineSlice(en.Inlines)
func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.List {
	tail := t.getInlineSlice(en.Inlines).Tail()
	if en.Syntax == meta.SyntaxSVG {
		tail = sxpf.NewPair(sxpf.NewString(string(en.Blob)), tail)
		tail = tail.Cons(sxpf.MakeString(string(en.Blob)))
	} else {
		tail = sxpf.NewPair(getBase64String(en.Blob), tail)
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return sxpf.NewPair(
		sexpr.SymEmbedBLOB,
		sxpf.NewPair(
			getAttributes(en.Attrs),
	return tail.Cons(sxpf.MakeString(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB)
			sxpf.NewPair(
				sxpf.NewString(en.Syntax),
				tail,
			),
		),
	)
}

var mapFormatKindS = map[ast.FormatKind]*sxpf.Symbol{
	ast.FormatEmph:   sexpr.SymFormatEmph,
	ast.FormatStrong: sexpr.SymFormatStrong,
	ast.FormatDelete: sexpr.SymFormatDelete,
	ast.FormatInsert: sexpr.SymFormatInsert,
	ast.FormatSuper:  sexpr.SymFormatSuper,
	ast.FormatSub:    sexpr.SymFormatSub,
	ast.FormatQuote:  sexpr.SymFormatQuote,
	ast.FormatSpan:   sexpr.SymFormatSpan,
}


var mapLiteralKindS = map[ast.LiteralKind]*sxpf.Symbol{
	ast.LiteralZettel:  sexpr.SymLiteralZettel,
	ast.LiteralProg:    sexpr.SymLiteralProg,
	ast.LiteralInput:   sexpr.SymLiteralInput,
	ast.LiteralOutput:  sexpr.SymLiteralOutput,
	ast.LiteralComment: sexpr.SymLiteralComment,
	ast.LiteralHTML:    sexpr.SymLiteralHTML,
	ast.LiteralMath:    sexpr.SymLiteralMath,
}

func (t *transformer) getBlockSlice(bs *ast.BlockSlice) *sxpf.Pair {
	lstVals := make([]sxpf.Value, len(*bs))
func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sxpf.List {
	objs := make([]sxpf.Object, len(*bs))
	for i, n := range *bs {
		lstVals[i] = t.getSexpr(n)
		objs[i] = t.GetSexpr(n)
	}
	return sxpf.NewPairFromSlice(lstVals)
	return sxpf.MakeList(objs...).Cons(t.zetSyms.SymBlock)
}
func (t *transformer) getInlineSlice(is ast.InlineSlice) *sxpf.Pair {
	lstVals := make([]sxpf.Value, len(is))
func (t *Transformer) getInlineSlice(is ast.InlineSlice) *sxpf.List {
	objs := make([]sxpf.Object, len(is))
	for i, n := range is {
		lstVals[i] = t.getSexpr(n)
		objs[i] = t.GetSexpr(n)
	}
	return sxpf.NewPairFromSlice(lstVals)
	return sxpf.MakeList(objs...).Cons(t.zetSyms.SymInline)
}

func getAttributes(a attrs.Attributes) sxpf.Value {
func (t *Transformer) getAttributes(a attrs.Attributes) sxpf.Object {
	if a.IsEmpty() {
		return sxpf.Nil()
	}
	keys := a.Keys()
	lstVals := make([]sxpf.Value, 0, len(keys))
	objs := make([]sxpf.Object, 0, len(keys))
	for _, k := range keys {
		lstVals = append(lstVals, sxpf.NewPair(sxpf.NewString(k), sxpf.NewPair(sxpf.NewString(a[k]), nil)))
		objs = append(objs, sxpf.Cons(sxpf.MakeString(k), sxpf.MakeString(a[k])))
	}
	return sxpf.NewPairFromSlice(lstVals)
}

	return sxpf.Nil().Cons(sxpf.MakeList(objs...)).Cons(t.zetSyms.SymQuote)
var mapRefStateS = map[ast.RefState]*sxpf.Symbol{
	ast.RefStateInvalid:  sexpr.SymRefStateInvalid,
	ast.RefStateZettel:   sexpr.SymRefStateZettel,
	ast.RefStateSelf:     sexpr.SymRefStateSelf,
	ast.RefStateFound:    sexpr.SymRefStateFound,
	ast.RefStateBroken:   sexpr.SymRefStateBroken,
	ast.RefStateHosted:   sexpr.SymRefStateHosted,
	ast.RefStateBased:    sexpr.SymRefStateBased,
	ast.RefStateQuery:    sexpr.SymRefStateQuery,
	ast.RefStateExternal: sexpr.SymRefStateExternal,
}

func getReference(ref *ast.Reference) *sxpf.Pair {
	return sxpf.NewPair(
		mapGetS(mapRefStateS, ref.State),
func (t *Transformer) getReference(ref *ast.Reference) *sxpf.List {
	return sxpf.MakeList(
		t.zetSyms.SymQuote,
		sxpf.MakeList(
			mapGetS(t, t.mapRefStateS, ref.State),
		sxpf.NewPair(
			sxpf.NewString(ref.Value),
			sxpf.Nil()))
			sxpf.MakeString(ref.Value),
		),
}

	)
var mapMetaTypeS = map[*meta.DescriptionType]*sxpf.Symbol{
	meta.TypeCredential:   sexpr.SymTypeCredential,
	meta.TypeEmpty:        sexpr.SymTypeEmpty,
	meta.TypeID:           sexpr.SymTypeID,
	meta.TypeIDSet:        sexpr.SymTypeIDSet,
	meta.TypeNumber:       sexpr.SymTypeNumber,
	meta.TypeString:       sexpr.SymTypeString,
	meta.TypeTagSet:       sexpr.SymTypeTagSet,
	meta.TypeTimestamp:    sexpr.SymTypeTimestamp,
	meta.TypeURL:          sexpr.SymTypeURL,
	meta.TypeWord:         sexpr.SymTypeWord,
	meta.TypeWordSet:      sexpr.SymTypeWordSet,
	meta.TypeZettelmarkup: sexpr.SymTypeZettelmarkup,
}

func GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.Pair {
func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.List {
	pairs := m.ComputedPairs()
	lstVals := make([]sxpf.Value, 0, len(pairs))
	objs := make([]sxpf.Object, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		strKey := sxpf.NewString(key)
		var val sxpf.Value
		symType := mapGetS(t, t.mapMetaTypeS, ty)
		strKey := sxpf.MakeString(key)
		var obj sxpf.Object
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setVals := make([]sxpf.Value, len(setList))
			setObjs := make([]sxpf.Object, len(setList))
			for i, val := range setList {
				setVals[i] = sxpf.NewString(val)
				setObjs[i] = sxpf.MakeString(val)
			}
			val = sxpf.NewPairFromSlice(setVals)
			obj = sxpf.MakeList(setObjs...).Cons(t.zetSyms.SymList)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			t := transformer{}
			val = t.getSexpr(&is)
			obj = t.GetSexpr(&is)
		} else {
			val = sxpf.NewString(p.Value)
			obj = sxpf.MakeString(p.Value)
		}
		lstVals = append(lstVals, sxpf.NewPair(symType, sxpf.NewPair(strKey, sxpf.NewPair(val, nil))))
		objs = append(objs, sxpf.Nil().Cons(obj).Cons(strKey).Cons(symType))
	}
	return sxpf.NewPairFromSlice(lstVals)
	return sxpf.MakeList(objs...).Cons(t.zetSyms.SymMeta)
}

func mapGetS[T comparable](m map[T]*sxpf.Symbol, k T) *sxpf.Symbol {
func mapGetS[T comparable](t *Transformer, m map[T]*sxpf.Symbol, k T) *sxpf.Symbol {
	if result, found := m[k]; found {
		return result
	}
	log.Println("MISS", k, m)
	return sexpr.Smk.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k))
	return t.sf.MustMake(fmt.Sprintf("**%v:NOT-FOUND**", k))
}

func getBase64String(data []byte) *sxpf.String {
	var buf bytes.Buffer
	encoder := base64.NewEncoder(base64.StdEncoding, &buf)
func getBase64String(data []byte) sxpf.String {
	var sb strings.Builder
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sxpf.NewString(buf.String())
		return sxpf.MakeString(sb.String())
	}
	return nil
	return sxpf.MakeString("")
}

Added encoder/shtmlenc/shtmlenc.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML.
package shtmlenc

import (
	"io"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/sexprenc"
)

func init() {
	encoder.Register(api.EncoderSHTML, func() encoder.Encoder { return Create() })
}

// Create a SHTML encoder
func Create() *Encoder { return &mySE }

type Encoder struct{}

var mySE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	tx := sexprenc.NewTransformer()
	th := shtml.NewTransformer(1, nil)
	metaSHTML, err := th.Transform(tx.GetMeta(zn.InhMeta, evalMeta))
	if err != nil {
		return 0, err
	}
	contentSHTML, err := th.Transform(tx.GetSexpr(&zn.Ast))
	if err != nil {
		return 0, err
	}
	result := sxpf.Cons(metaSHTML, contentSHTML)
	return result.Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	tx := sexprenc.NewTransformer()
	th := shtml.NewTransformer(1, nil)
	metaSHTML, err := th.Transform(tx.GetMeta(m, evalMeta))
	if err != nil {
		return 0, err
	}
	return metaSHTML.Print(w)
}

func (se *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return se.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	hval, err := TransformSlice(bs)
	if err != nil {
		return 0, err
	}
	return hval.Print(w)
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	hval, err := TransformSlice(is)
	if err != nil {
		return 0, err
	}
	return hval.Print(w)
}

// TransformSlice transforms a AST slice into SHTML.
func TransformSlice(node ast.Node) (*sxpf.List, error) {
	tx := sexprenc.NewTransformer()
	xval := tx.GetSexpr(node)
	th := shtml.NewTransformer(1, nil)
	return th.Transform(xval)
}

Changes to encoder/textenc/textenc.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to encoder/write.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Deleted encoder/zjsonenc/zjsonenc.go.

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




















































































































































































































































































































































































































































































































































































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

// Package zjsonenc encodes the abstract syntax tree into JSON.
//
// Deprecated in v0.11
package zjsonenc

import (
	"fmt"
	"io"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/strfun"
)

func init() {
	encoder.Register(api.EncoderZJSON, func() encoder.Encoder { return Create() })
}

// Create a ZJSON encoder
func Create() *Encoder { return &myJE }

type Encoder struct{}

var myJE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newDetailVisitor(w)
	v.b.WriteString(`{"meta":`)
	v.writeMeta(zn.InhMeta, evalMeta)
	v.b.WriteString(`,"content":`)
	ast.Walk(v, &zn.Ast)
	v.b.WriteByte('}')
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as JSON.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newDetailVisitor(w)
	v.writeMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (je *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return je.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newDetailVisitor(w)
	ast.Walk(v, bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newDetailVisitor(w)
	ast.Walk(v, is)
	length, err := v.b.Flush()
	return length, err
}

// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
	b       encoder.EncWriter
	inVerse bool // Visiting a verse block: save spaces in ZJSON object
}

func newDetailVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewEncWriter(w)} }

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
		return nil
	case *ast.InlineSlice:
		v.walkInlineSlice(n)
		return nil
	case *ast.ParaNode:
		v.writeNodeStart(zjson.TypeParagraph)
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &n.Inlines)
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.writeNodeStart(zjson.TypeBreakThematic)
		v.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.TranscludeNode:
		v.writeNodeStart(zjson.TypeTransclude)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameString2)
		writeEscaped(&v.b, mapRefState[n.Ref.State])
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, n.Ref.String())
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.writeNodeStart(zjson.TypeText)
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, n.Text)
	case *ast.SpaceNode:
		v.writeNodeStart(zjson.TypeSpace)
		if v.inVerse {
			v.writeContentStart(zjson.NameString)
			writeEscaped(&v.b, n.Lexeme)
		}
	case *ast.BreakNode:
		if n.Hard {
			v.writeNodeStart(zjson.TypeBreakHard)
		} else {
			v.writeNodeStart(zjson.TypeBreakSoft)
		}
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOB(n)
	case *ast.CiteNode:
		v.writeNodeStart(zjson.TypeCitation)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, n.Key)
		if len(n.Inlines) > 0 {
			v.writeContentStart(zjson.NameInline)
			ast.Walk(v, &n.Inlines)
		}
	case *ast.FootnoteNode:
		v.writeNodeStart(zjson.TypeFootnote)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &n.Inlines)
	case *ast.MarkNode:
		v.visitMark(n)
	case *ast.FormatNode:
		v.writeNodeStart(mapFormatKind[n.Kind])
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &n.Inlines)
	case *ast.LiteralNode:
		kind, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown literal kind %v", n.Kind))
		}
		v.writeNodeStart(kind)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, string(n.Content))
	default:
		return v
	}
	v.b.WriteByte('}')
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  zjson.TypeVerbatimZettel,
	ast.VerbatimProg:    zjson.TypeVerbatimCode,
	ast.VerbatimEval:    zjson.TypeVerbatimEval,
	ast.VerbatimMath:    zjson.TypeVerbatimMath,
	ast.VerbatimComment: zjson.TypeVerbatimComment,
	ast.VerbatimHTML:    zjson.TypeVerbatimHTML,
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind))
	}
	v.writeNodeStart(kind)
	v.visitAttributes(vn.Attrs)
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, string(vn.Content))
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  zjson.TypeBlock,
	ast.RegionQuote: zjson.TypeExcerpt,
	ast.RegionVerse: zjson.TypePoem,
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %v", rn.Kind))
	}
	saveInVerse := v.inVerse
	if rn.Kind == ast.RegionVerse {
		v.inVerse = true
	}
	v.writeNodeStart(kind)
	v.visitAttributes(rn.Attrs)
	v.writeContentStart(zjson.NameBlock)
	ast.Walk(v, &rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &rn.Inlines)
	}
	v.inVerse = saveInVerse
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	v.writeNodeStart(zjson.TypeHeading)
	v.visitAttributes(hn.Attrs)
	v.writeContentStart(zjson.NameNumeric)
	v.b.WriteString(strconv.Itoa(hn.Level))
	if fragment := hn.Fragment; fragment != "" {
		v.writeContentStart(zjson.NameString)
		v.b.WriteStrings(`"`, fragment, `"`)
	}
	v.writeContentStart(zjson.NameInline)
	ast.Walk(v, &hn.Inlines)
}

var mapNestedListKind = map[ast.NestedListKind]string{
	ast.NestedListOrdered:   zjson.TypeListOrdered,
	ast.NestedListUnordered: zjson.TypeListBullet,
	ast.NestedListQuote:     zjson.TypeListQuotation,
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	v.writeNodeStart(mapNestedListKind[ln.Kind])
	v.writeContentStart(zjson.NameList)
	for i, item := range ln.Items {
		v.writeComma(i)
		v.b.WriteByte('[')
		for j, in := range item {
			v.writeComma(j)
			ast.Walk(v, in)
		}
		v.b.WriteByte(']')
	}
	v.b.WriteByte(']')
}

func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	v.writeNodeStart(zjson.TypeDescrList)
	v.writeContentStart(zjson.NameDescrList)
	for i, def := range dn.Descriptions {
		v.writeComma(i)
		v.b.WriteStrings(`{"`, zjson.NameInline, `":`)
		ast.Walk(v, &def.Term)

		if len(def.Descriptions) > 0 {
			v.writeContentStart(zjson.NameDescription)
			for j, b := range def.Descriptions {
				v.writeComma(j)
				v.b.WriteByte('[')
				for k, dn := range b {
					v.writeComma(k)
					ast.Walk(v, dn)
				}
				v.b.WriteByte(']')
			}
			v.b.WriteByte(']')
		}
		v.b.WriteByte('}')
	}
	v.b.WriteByte(']')
}

func (v *visitor) visitTable(tn *ast.TableNode) {
	v.writeNodeStart(zjson.TypeTable)
	v.writeContentStart(zjson.NameTable)

	// Table header
	v.b.WriteByte('[')
	for i, cell := range tn.Header {
		v.writeComma(i)
		v.writeCell(cell)
	}
	v.b.WriteString("],")

	// Table rows
	v.b.WriteByte('[')
	for i, row := range tn.Rows {
		v.writeComma(i)
		v.b.WriteByte('[')
		for j, cell := range row {
			v.writeComma(j)
			v.writeCell(cell)
		}
		v.b.WriteByte(']')
	}
	v.b.WriteString("]]")
}

var alignmentCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
	ast.AlignCenter:  ":",
	ast.AlignRight:   ">",
}

func (v *visitor) writeCell(cell *ast.TableCell) {
	if aCode := alignmentCode[cell.Align]; aCode != "" {
		v.b.WriteStrings(`{"`, zjson.NameString, `":"`, aCode, `","`, zjson.NameInline, `":`)
	} else {
		v.b.WriteStrings(`{"`, zjson.NameInline, `":`)
	}
	ast.Walk(v, &cell.Inlines)
	v.b.WriteByte('}')
}

func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
	v.writeNodeStart(zjson.TypeBLOB)
	if len(bn.Description) > 0 {
		v.writeContentStart(zjson.NameString2)
		ast.Walk(v, &bn.Description)
	}
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, bn.Syntax)
	if bn.Syntax == meta.SyntaxSVG {
		v.writeContentStart(zjson.NameString3)
		writeEscaped(&v.b, string(bn.Blob))
	} else {
		v.writeContentStart(zjson.NameBinary)
		v.b.WriteBase64(bn.Blob)
		v.b.WriteByte('"')
	}
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:  zjson.RefStateInvalid,
	ast.RefStateZettel:   zjson.RefStateZettel,
	ast.RefStateSelf:     zjson.RefStateSelf,
	ast.RefStateFound:    zjson.RefStateFound,
	ast.RefStateBroken:   zjson.RefStateBroken,
	ast.RefStateHosted:   zjson.RefStateHosted,
	ast.RefStateBased:    zjson.RefStateBased,
	ast.RefStateQuery:    zjson.RefStateQuery,
	ast.RefStateExternal: zjson.RefStateExternal,
}

func (v *visitor) visitLink(ln *ast.LinkNode) {
	v.writeNodeStart(zjson.TypeLink)
	v.visitAttributes(ln.Attrs)
	v.writeContentStart(zjson.NameString2)
	writeEscaped(&v.b, mapRefState[ln.Ref.State])
	v.writeContentStart(zjson.NameString)
	if ln.Ref.State == ast.RefStateQuery {
		writeEscaped(&v.b, ln.Ref.Value)
	} else {
		writeEscaped(&v.b, ln.Ref.String())
	}
	if len(ln.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &ln.Inlines)
	}
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.writeNodeStart(zjson.TypeEmbed)
	v.visitAttributes(en.Attrs)
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, en.Ref.String())

	if len(en.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &en.Inlines)
	}
	if en.Syntax != "" {
		v.writeContentStart(zjson.NameString2)
		writeEscaped(&v.b, en.Syntax)
	}
}

func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	v.writeNodeStart(zjson.TypeEmbedBLOB)
	v.visitAttributes(en.Attrs)
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, en.Syntax)
	if en.Syntax == meta.SyntaxSVG {
		v.writeContentStart(zjson.NameString3)
		writeEscaped(&v.b, string(en.Blob))
	} else {
		v.writeContentStart(zjson.NameBinary)
		v.b.WriteBase64(en.Blob)
		v.b.WriteByte('"')
	}
	if len(en.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &en.Inlines)
	}
}

func (v *visitor) visitMark(mn *ast.MarkNode) {
	v.writeNodeStart(zjson.TypeMark)
	if text := mn.Mark; text != "" {
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, text)
	}
	if fragment := mn.Fragment; fragment != "" {
		v.writeContentStart(zjson.NameString2)
		v.b.WriteByte('"')
		v.b.WriteString(fragment)
		v.b.WriteByte('"')
	}
	if len(mn.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &mn.Inlines)
	}
}

var mapFormatKind = map[ast.FormatKind]string{
	ast.FormatEmph:   zjson.TypeFormatEmph,
	ast.FormatStrong: zjson.TypeFormatStrong,
	ast.FormatDelete: zjson.TypeFormatDelete,
	ast.FormatInsert: zjson.TypeFormatInsert,
	ast.FormatSuper:  zjson.TypeFormatSuper,
	ast.FormatSub:    zjson.TypeFormatSub,
	ast.FormatQuote:  zjson.TypeFormatQuote,
	ast.FormatSpan:   zjson.TypeFormatSpan,
}

var mapLiteralKind = map[ast.LiteralKind]string{
	ast.LiteralZettel:  zjson.TypeLiteralZettel,
	ast.LiteralProg:    zjson.TypeLiteralCode,
	ast.LiteralInput:   zjson.TypeLiteralInput,
	ast.LiteralOutput:  zjson.TypeLiteralOutput,
	ast.LiteralComment: zjson.TypeLiteralComment,
	ast.LiteralHTML:    zjson.TypeLiteralHTML,
	ast.LiteralMath:    zjson.TypeLiteralMath,
}

func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
	v.b.WriteByte('[')
	for i, bn := range *bs {
		v.writeComma(i)
		ast.Walk(v, bn)
	}
	v.b.WriteByte(']')
}

func (v *visitor) walkInlineSlice(is *ast.InlineSlice) {
	v.b.WriteByte('[')
	for i, in := range *is {
		v.writeComma(i)
		ast.Walk(v, in)
	}
	v.b.WriteByte(']')
}

// visitAttributes write JSON attributes
func (v *visitor) visitAttributes(a attrs.Attributes) {
	if a.IsEmpty() {
		return
	}

	v.writeContentStart(zjson.NameAttribute)
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteString(`","`)
		}
		strfun.JSONEscape(&v.b, k)
		v.b.WriteString(`":"`)
		strfun.JSONEscape(&v.b, a[k])
	}
	v.b.WriteString(`"}`)
}

func (v *visitor) writeNodeStart(t string) {
	v.b.WriteStrings(`{"":"`, t, `"`)
}

var valueStart = map[string]string{
	zjson.NameBlock:       "",
	zjson.NameAttribute:   `{"`,
	zjson.NameList:        "[",
	zjson.NameDescrList:   "[",
	zjson.NameDescription: "[",
	zjson.NameInline:      "",
	zjson.NameBLOB:        "{",
	zjson.NameNumeric:     "",
	zjson.NameBinary:      `"`,
	zjson.NameTable:       "[",
	zjson.NameString2:     "",
	zjson.NameString:      "",
	zjson.NameString3:     "",
}

func (v *visitor) writeContentStart(jsonName string) {
	s, ok := valueStart[jsonName]
	if !ok {
		panic("Unknown object name " + jsonName)
	}
	v.b.WriteStrings(`,"`, jsonName, `":`, s)
}

func (v *visitor) writeMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	v.b.WriteByte('{')
	for i, p := range m.ComputedPairs() {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.b.WriteByte('"')
		key := p.Key
		strfun.JSONEscape(&v.b, key)
		t := m.Type(key)
		v.b.WriteStrings(`":{"`, zjson.NameType, `":"`, t.Name, `","`)
		if t.IsSet {
			v.b.WriteStrings(zjson.NameSet, `":`)
			v.writeSetValue(p.Value)
		} else if t == meta.TypeZettelmarkup {
			v.b.WriteStrings(zjson.NameInline, `":`)
			is := evalMeta(p.Value)
			ast.Walk(v, &is)
		} else {
			v.b.WriteStrings(zjson.NameString, `":`)
			writeEscaped(&v.b, p.Value)
		}
		v.b.WriteByte('}')
	}
	v.b.WriteByte('}')
}

func (v *visitor) writeSetValue(value string) {
	v.b.WriteByte('[')
	for i, val := range meta.ListFromValue(value) {
		v.writeComma(i)
		writeEscaped(&v.b, val)
	}
	v.b.WriteByte(']')
}

func (v *visitor) writeComma(pos int) {
	if pos > 0 {
		v.b.WriteByte(',')
	}
}

func writeEscaped(b *encoder.EncWriter, s string) {
	b.WriteByte('"')
	strfun.JSONEscape(b, s)
	b.WriteByte('"')
}

Changes to encoder/zmkenc/zmkenc.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
1

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

15
16
17
18
19
20
21
22
23
24

-
+












-


+







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

// Package zmkenc encodes the abstract syntax tree back into Zettelmarkup.
package zmkenc

import (
	"bytes"
	"fmt"
	"io"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
285
286
287
288
289
290
291




292


293
294
295
296
297
298
299
285
286
287
288
289
290
291
292
293
294
295

296
297
298
299
300
301
302
303
304







+
+
+
+
-
+
+







			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 {
				if jj > 0 {
					v.b.WriteString("\n\n  ")
				}
			ast.WalkDescriptionSlice(v, b)
				ast.Walk(v, dn)
			}
		}
	}
}

var alignCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
341
342
343
344
345
346
347
348
349
350



351
352
353
354
355
356
357
346
347
348
349
350
351
352



353
354
355
356
357
358
359
360
361
362







-
-
-
+
+
+







func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
	if bn.Syntax == meta.SyntaxSVG {
		v.b.WriteStrings("@@@", bn.Syntax, "\n")
		v.b.Write(bn.Blob)
		v.b.WriteString("\n@@@\n")
		return
	}
	var buf bytes.Buffer
	v.textEnc.WriteInlines(&buf, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", buf.String(), "' and syntax '", bn.Syntax, "'.")
	var sb strings.Builder
	v.textEnc.WriteInlines(&sb, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
}

var escapeSeqs = strfun.NewSet(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==",
)

func (v *visitor) visitText(tn *ast.TextNode) {
389
390
391
392
393
394
395



396
397
398
399
400
401
402
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410







+
+
+







}

func (v *visitor) 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 *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.b.WriteString("{{")
	if len(en.Inlines) > 0 {
415
416
417
418
419
420
421
422

423
424
425
426
427
428
429
423
424
425
426
427
428
429

430
431
432
433
434
435
436
437







-
+







	}
	v.b.WriteString("{{TODO: display inline BLOB}}")
}

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

func (v *visitor) visitMark(mn *ast.MarkNode) {

Changes to encoding/atom/atom.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to encoding/encoding.go.

1
2

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

-
+












-





-







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

// Package encoding provides helper functions for encodings.
package encoding

import (
	"bytes"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
)

// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
	maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local)
	for _, m := range ml {
36
37
38
39
40
41
42
43
44
45
46

47
48
49
50
51
52
53
34
35
36
37
38
39
40


41

42














-
-

-
+
-
-
-
-
-
-
-
	}
	if maxPublished.Year() > 1 {
		return maxPublished.UTC().Format(timeFormat)
	}
	return ""
}

var textEnc = textenc.Create()

// TitleAsText returns the title of a zettel as plain text
func TitleAsText(m *meta.Meta) string {
func TitleAsText(m *meta.Meta) string { return parser.NormalizedSpacedText(m.GetTitle()) }
	var title bytes.Buffer
	titleIns := parser.ParseMetadata(m.GetTitle())
	if _, err := textEnc.WriteInlines(&title, &titleIns); err != nil {
		return m.GetTitle()
	}
	return title.String()
}

Changes to encoding/rss/rss.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to encoding/xml/xml.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to evaluator/evaluator.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to evaluator/list.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255


256
257
258
259
260
261
262
240
241
242
243
244
245
246





247
248


249
250
251
252
253
254
255
256
257







-
-
-
-
-


-
-
+
+







	items := make([]ast.ItemSlice, 0, len(ap.ml))
	for _, m := range ap.ml {
		if key != "" {
			if _, found := m.Get(key); !found {
				continue
			}
		}
		zid := m.Zid.String()
		title, found := m.Get(api.KeyTitle)
		if !found {
			title = zid
		}
		items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
			Attrs:   nil,
			Ref:     ast.ParseReference(zid),
			Inlines: parser.ParseMetadataNoLink(title),
			Ref:     ast.ParseReference(m.Zid.String()),
			Inlines: parser.ParseSpacedText(m.GetTitle()),
		})})
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		Items: items,
		Attrs: nil,
	}

Changes to evaluator/metadata.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to go.mod.

1
2
3
4
5

6

7
8
9
10
11
12
13





14
15
16

1
2
3
4
5
6

7
8
9





10
11
12
13
14
15
16

17





+
-
+


-
-
-
-
-
+
+
+
+
+


-
+
module zettelstore.de/z

go 1.19

require (
	codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197
	codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0
	codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475
	github.com/fsnotify/fsnotify v1.6.0
	github.com/pascaldekloe/jwt v1.12.0
	github.com/yuin/goldmark v1.5.3
	golang.org/x/crypto v0.5.0
	golang.org/x/term v0.4.0
	golang.org/x/text v0.6.0
	zettelstore.de/c v0.10.0
	github.com/yuin/goldmark v1.5.4
	golang.org/x/crypto v0.7.0
	golang.org/x/term v0.6.0
	golang.org/x/text v0.8.0
	zettelstore.de/c v0.11.0
)

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








1
2


3
4
5
6
7
8




9
10
11
12
13








14
15
16
17
18
19
20
21
+
+
-
-
+
+




-
-
-
-
+
+
+
+

-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 h1:6kX7TY25agLFlHNvByO1Jc3GrBA7mu7aOa8tCOniUew=
codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197/go.mod h1:Dp3EwBSsE3TvdPw9QZ4Wm25ZragluVT2OayRFRiq6jk=
codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 h1:viya/OgeF16+i8caBPJmcLQhGpZodPh+/nxtJzSSO1s=
codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0/go.mod h1:4fAHEF3VH+ofbZkF6NzqiItTNy2X11tVCnZX99jXouA=
codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 h1:0OTzV3FYY/Y7YsaVaSzF4Wd17pXzdH6DaSvMeqteJc4=
codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475/go.mod h1:iSbMygOmtRQYp8pryNKYzRuMibYDSR80smU2b6qm1bc=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/pascaldekloe/jwt v1.12.0 h1:imQSkPOtAIBAXoKKjL9ZVJuF/rVqJ+ntiLGpLyeqMUQ=
github.com/pascaldekloe/jwt v1.12.0/go.mod h1:LiIl7EwaglmH1hWThd/AmydNCnHf/mmfluBlNqHbk8U=
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
zettelstore.de/c v0.10.0 h1:e5VOrH5aEEojNrUfO/721dsiHsp/yuTvNPF6DNsgA3U=
zettelstore.de/c v0.10.0/go.mod h1:+SoneUhKQ81A2Id/bC6FdDYYQAHYfVryh7wHFnnklew=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
zettelstore.de/c v0.11.0 h1:eD1og+5R2DC3po8JEYUKldONxo69uIKm0jmbNH+NfZI=
zettelstore.de/c v0.11.0/go.mod h1:aYfHJ/ipU9BvYVBGVwOyfFjuGpf29Pf5Xn+pjcutR6E=

Changes to input/entity.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to input/entity_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to input/input.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to input/input_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to input/runes.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to kernel/impl/auth.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/box.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/cfg.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
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







-
+
-
-
-
-
+
+
+
















-







				case kernel.ConfigZmkHTML:
					return config.ZettelmarkupHTML, nil
				}
				return config.NoHTML, nil
			}),
			true,
		},
		api.KeyLang:              {"Language", parseString, true},
		api.KeyLang:         {"Language", parseString, true},
		config.KeyMarkerExternal: {"Marker external URL", parseString, true},
		keyMaxTransclusions:      {"Maximum transclusions", parseInt64, true},
		keySiteName:              {"Site name", parseString, true},
		keyYAMLHeader:            {"YAML header", parseBool, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},
		kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true},
	}
	cs.next = interfaceMap{
		keyDefaultCopyright:       "",
		keyDefaultLicense:         "",
		keyDefaultVisibility:      meta.VisibilityLogin,
		keyExpertMode:             false,
		config.KeyFooterZettel:    id.Invalid,
		config.KeyHomeZettel:      id.DefaultHomeZid,
		kernel.ConfigInsecureHTML: config.NoHTML,
		api.KeyLang:               api.ValueLangEN,
		config.KeyMarkerExternal:  "&#10138;",
		keyMaxTransclusions:       int64(1024),
		keySiteName:               "Zettelstore",
		keyYAMLHeader:             false,
		keyZettelFileSyntax:       nil,
		kernel.ConfigSimpleMode:   false,
	}
}
141
142
143
144
145
146
147
148

149
150
151

152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175

176
177
178
179
180
181
182
139
140
141
142
143
144
145

146
147
148

149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169




170
171
172
173
174
175
176
177







-
+


-
+




















-
-
-
-
+








func (*configService) GetStatistics() []kernel.KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {
	mgr.RegisterObserver(cs.observe)
	cs.doUpdate(mgr)
	cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
}

func (cs *configService) doUpdate(p box.Box) error {
func (cs *configService) doUpdate(p box.BaseBox) error {
	m, err := p.GetMeta(context.Background(), cs.orig.Zid)
	cs.logger.Trace().Err(err).Msg("got config meta")
	if err != nil {
		return err
	}
	cs.mxService.Lock()
	for _, pair := range cs.orig.Pairs() {
		key := pair.Key
		if val, ok := m.Get(key); ok {
			cs.SetConfig(key, val)
		} else if defVal, defFound := cs.orig.Get(key); defFound {
			cs.SetConfig(key, defVal)
		}
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if ci.Reason == box.OnReload {
		cs.logger.Debug().Msg("reload")
		go func() { cs.doUpdate(ci.Box) }()
	} else if ci.Zid == id.ConfigurationZid {
	if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() { cs.doUpdate(ci.Box) }()
	}
}

// --- config.Config

Changes to kernel/impl/cmd.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/config.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/core.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/impl.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/log.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/server.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/impl/web.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to kernel/kernel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to logger/logger.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to logger/logger_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to logger/message.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to parser/blob/blob.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/cleaner/cleaner.go.

1
2

3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1

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

15
16
17
18
19
20
21
22
23

-
+












-

+







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

// Package cleaner provides functions to clean up the parsed AST.
package cleaner

import (
	"bytes"
	"strconv"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
)

// CleanBlockSlice cleans the given block list.
133
134
135
136
137
138
139
140
141


142
143
144
145

146
147
148
149
150
151
152
133
134
135
136
137
138
139


140
141
142
143
144

145
146
147
148
149
150
151
152







-
-
+
+



-
+







}

func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
		return
	}
	if hn.Slug == "" {
		var buf bytes.Buffer
		_, err := cv.textEnc.WriteInlines(&buf, &hn.Inlines)
		var sb strings.Builder
		_, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
		if err != nil {
			return
		}
		hn.Slug = strfun.Slugify(buf.String())
		hn.Slug = strfun.Slugify(sb.String())
	}
	if hn.Slug != "" {
		hn.Fragment = cv.addIdentifier(hn.Slug, hn)
	}
}

func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) {

Changes to parser/draw/canvas.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//

Changes to parser/draw/canvas_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//

Changes to parser/draw/char.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//

Changes to parser/draw/draw.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to parser/draw/draw_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to parser/draw/object.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//

Changes to parser/draw/point.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//

Changes to parser/draw/svg.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//
159
160
161
162
163
164
165
166
167
168



169
170
171
172
173
174
175
159
160
161
162
163
164
165



166
167
168
169
170
171
172
173
174
175







-
-
-
+
+
+







	}
	if !first {
		io.WriteString(w, "</g>")
	}
}

func escape(s string) string {
	b := bytes.Buffer{}
	strfun.XMLEscape(&b, s)
	return b.String()
	var sb strings.Builder
	strfun.XMLEscape(&sb, s)
	return sb.String()
}

type scaledPoint struct {
	X    float64
	Y    float64
	Hint renderHint
}

Changes to parser/draw/svg_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//

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

-
+















+







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

// Package markdown provides a parser for markdown.
package markdown

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"

	gm "github.com/yuin/goldmark"
	gmAst "github.com/yuin/goldmark/ast"
	gmText "github.com/yuin/goldmark/text"

	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
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
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







-
+







-
-
+
+






-
-
+
+





-
+

-
+







	';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {},
	']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {},
}

// cleanText removes backslashes from TextNodes and expands entities
func cleanText(text []byte, cleanBS bool) string {
	lastPos := 0
	var buf bytes.Buffer
	var sb strings.Builder
	for pos, ch := range text {
		if pos < lastPos {
			continue
		}
		if ch == '&' {
			inp := input.NewInput([]byte(text[pos:]))
			if s, ok := inp.ScanEntity(); ok {
				buf.Write(text[lastPos:pos])
				buf.WriteString(s)
				sb.Write(text[lastPos:pos])
				sb.WriteString(s)
				lastPos = pos + inp.Pos
			}
			continue
		}
		if cleanBS && ch == '\\' && pos < len(text)-1 {
			if _, found := ignoreAfterBS[text[pos+1]]; found {
				buf.Write(text[lastPos:pos])
				buf.WriteByte(text[pos+1])
				sb.Write(text[lastPos:pos])
				sb.WriteByte(text[pos+1])
				lastPos = pos + 2
			}
		}
	}
	if lastPos < len(text) {
		buf.Write(text[lastPos:])
		sb.Write(text[lastPos:])
	}
	return buf.String()
	return sb.String()
}

func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice {
	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:    ast.LiteralProg,
			Attrs:   nil, //TODO
432
433
434
435
436
437
438
439
440


441
442
443
444

445
446
447

448
449
450
451
452
453
454
433
434
435
436
437
438
439


440
441
442
443
444

445
446
447

448
449
450
451
452
453
454
455







-
-
+
+



-
+


-
+







			Attrs:   a,
		},
	}
}

func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice {
	is := p.acceptInlineChildren(node)
	var buf bytes.Buffer
	_, err := p.textEnc.WriteInlines(&buf, &is)
	var sb strings.Builder
	_, err := p.textEnc.WriteInlines(&sb, &is)
	if err != nil {
		panic(err)
	}
	if buf.Len() == 0 {
	if sb.Len() == 0 {
		return nil
	}
	return ast.InlineSlice{&ast.TextNode{Text: buf.String()}}
	return ast.InlineSlice{&ast.TextNode{Text: sb.String()}}
}

func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice {
	u := node.URL(p.source)
	if node.AutoLinkType == gmAst.AutoLinkEmail &&
		!bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) {
		u = append([]byte("mailto:"), u...)

Changes to parser/markdown/markdown_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
1

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

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

34
35
36
37


38
39
40


41
42
43

44
45
46

47
48
49
50
51
52

-
+











-
+



















-
+



-
-
+
+

-
-
+
+

-
+


-
+





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

package markdown

import (
	"bytes"
	"strings"
	"testing"

	"zettelstore.de/z/ast"
)

func TestSplitText(t *testing.T) {
	t.Parallel()
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"abc", "Tabc"},
		{" ", "S "},
		{"abc def", "TabcS Tdef"},
		{"abc def ", "TabcS TdefS "},
		{" abc def ", "S TabcS TdefS "},
	}
	for i, tc := range testcases {
		var buf bytes.Buffer
		var sb strings.Builder
		for _, in := range splitText(tc.text) {
			switch n := in.(type) {
			case *ast.TextNode:
				buf.WriteByte('T')
				buf.WriteString(n.Text)
				sb.WriteByte('T')
				sb.WriteString(n.Text)
			case *ast.SpaceNode:
				buf.WriteByte('S')
				buf.WriteString(n.Lexeme)
				sb.WriteByte('S')
				sb.WriteString(n.Lexeme)
			default:
				buf.WriteByte('Q')
				sb.WriteByte('Q')
			}
		}
		got := buf.String()
		got := sb.String()
		if tc.exp != got {
			t.Errorf("TC=%d, text=%q, exp=%q, got=%q", i, tc.text, tc.exp, got)
		}
	}
}

Changes to parser/none/none.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/parser.go.

1
2

3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1

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

-
+














+







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

// Package parser provides a generic interface to a range of different parsers.
package parser

import (
	"context"
	"fmt"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
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
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







-
-
-
+
+
+
-
-
-
+


+
+
+





-
+
-
-
+
+
+

-
-
+
+

-
+








// ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice.
// Typically used to parse the title or other metadata of type Zettelmarkup.
func ParseMetadata(value string) ast.InlineSlice {
	return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk)
}

// ParseMetadataNoLink parses a string as Zettelmarkup, resulting in an inline slice.
// All link and footnote nodes will be removed.
func ParseMetadataNoLink(value string) ast.InlineSlice {
// ParseSpacedText returns an inline slice that consists just of test and space node.
// No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice.
func ParseSpacedText(s string) ast.InlineSlice {
	in := ParseMetadata(value)
	cleaner.CleanInlineLinks(&in)
	return in
	return ast.CreateInlineSliceFromWords(meta.ListFromValue(s)...)
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") }

// ParseDescription returns a suitable description stored in the metadata as an inline slice.
func ParseDescription(m *meta.Meta) ast.InlineSlice {
	if m == nil {
		return nil
	}
	descr, found := m.Get(api.KeySummary)
	if descr, found := m.Get(api.KeySummary); found {
	if !found {
		descr, found = m.Get(api.KeyTitle)
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if !found {
		return nil
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return ParseMetadataNoLink(descr)
	return nil
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {

Changes to parser/parser_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to parser/plain/plain.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/zettelmark/block.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/zettelmark/inline.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/zettelmark/node.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/zettelmark/post-processor.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/zettelmark/zettelmark.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to parser/zettelmark/zettelmark_fuzz_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to parser/zettelmark/zettelmark_test.go.

1
2

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

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

15
16
17
18
19
20
21

-
+












-







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

// Package zettelmark_test provides some tests for the zettelmarkup parser.
package zettelmark_test

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
762
763
764
765
766
767
768
769

770
771
772

773
774
775
776
777
778
779

780
781

782
783
784
785
786
787

788
789
790


791
792

793
794
795
796
797
798
799

800
801

802
803
804
805

806
807

808
809

810
811
812

813
814
815
816


817
818

819
820
821

822
823
824

825
826

827
828

829
830

831
832

833
834

835
836

837
838

839
840

841
842
843

844
845

846
847

848
849
850


851
852

853
854

855
856
857

858
859

860
861
862

863
864
865


866
867

868
869

870
871
872

873
874

875
876
877
878
879



880
881

882
883
884

885
886

887
888
889
890

891
892

893
894
895

896
897

898
899
900

901
902
903
904

905
906
907
908
909

910
911
912
913

914
915
916

917
918

919
920
921

922
923
924
925



926
927
928
929


930
931
932
933
934

935
936

937
938

939
940
941
942
943
944
945
946


947
948
949


950
951

952
953
954
955
956
957
958
761
762
763
764
765
766
767

768
769
770

771
772
773
774
775
776
777

778
779

780
781
782
783
784
785

786
787


788
789
790

791
792
793
794
795
796
797

798
799

800
801
802
803

804
805

806
807

808
809
810

811
812
813


814
815
816

817
818
819

820
821
822

823
824

825
826

827
828

829
830

831
832

833
834

835
836

837
838

839
840
841

842
843

844
845

846
847


848
849
850

851
852

853
854
855

856
857

858
859
860

861
862


863
864
865

866
867

868
869
870

871
872

873
874
875



876
877
878
879

880
881
882

883
884

885
886
887
888

889
890

891
892
893

894
895

896
897
898

899
900
901
902

903
904
905
906
907

908
909
910
911

912
913
914

915
916

917
918
919

920
921



922
923
924
925
926


927
928
929
930
931
932

933
934

935
936

937
938
939
940
941
942
943


944
945
946


947
948
949

950
951
952
953
954
955
956
957







-
+


-
+






-
+

-
+





-
+

-
-
+
+

-
+






-
+

-
+



-
+

-
+

-
+


-
+


-
-
+
+

-
+


-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+


-
+

-
+

-
+

-
-
+
+

-
+

-
+


-
+

-
+


-
+

-
-
+
+

-
+

-
+


-
+

-
+


-
-
-
+
+
+

-
+


-
+

-
+



-
+

-
+


-
+

-
+


-
+



-
+




-
+



-
+


-
+

-
+


-
+

-
-
-
+
+
+


-
-
+
+




-
+

-
+

-
+






-
-
+
+

-
-
+
+

-
+







	})
}

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

// TestVisitor serializes the abstract syntax tree to a string.
type TestVisitor struct {
	buf bytes.Buffer
	sb strings.Builder
}

func (tv *TestVisitor) String() string { return tv.buf.String() }
func (tv *TestVisitor) String() string { return tv.sb.String() }

func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		tv.visitInlineSlice(n)
	case *ast.ParaNode:
		tv.buf.WriteString("(PARA")
		tv.sb.WriteString("(PARA")
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.VerbatimNode:
		code, ok := mapVerbatimKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind))
		}
		tv.buf.WriteString(code)
		tv.sb.WriteString(code)
		if len(n.Content) > 0 {
			tv.buf.WriteByte('\n')
			tv.buf.Write(n.Content)
			tv.sb.WriteByte('\n')
			tv.sb.Write(n.Content)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.RegionNode:
		code, ok := mapRegionKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown region code %v", n.Kind))
		}
		tv.buf.WriteString(code)
		tv.sb.WriteString(code)
		if len(n.Blocks) > 0 {
			tv.buf.WriteByte(' ')
			tv.sb.WriteByte(' ')
			ast.Walk(tv, &n.Blocks)
		}
		if len(n.Inlines) > 0 {
			tv.buf.WriteString(" (LINE")
			tv.sb.WriteString(" (LINE")
			ast.Walk(tv, &n.Inlines)
			tv.buf.WriteByte(')')
			tv.sb.WriteByte(')')
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HeadingNode:
		fmt.Fprintf(&tv.buf, "(H%d", n.Level)
		fmt.Fprintf(&tv.sb, "(H%d", n.Level)
		ast.Walk(tv, &n.Inlines)
		if n.Fragment != "" {
			tv.buf.WriteString(" #")
			tv.buf.WriteString(n.Fragment)
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HRuleNode:
		tv.buf.WriteString("(HR)")
		tv.sb.WriteString("(HR)")
		tv.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		tv.buf.WriteString(mapNestedListKind[n.Kind])
		tv.sb.WriteString(mapNestedListKind[n.Kind])
		for _, item := range n.Items {
			tv.buf.WriteString(" {")
			tv.sb.WriteString(" {")
			ast.WalkItemSlice(tv, item)
			tv.buf.WriteByte('}')
			tv.sb.WriteByte('}')
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.DescriptionListNode:
		tv.buf.WriteString("(DL")
		tv.sb.WriteString("(DL")
		for _, def := range n.Descriptions {
			tv.buf.WriteString(" (DT")
			tv.sb.WriteString(" (DT")
			ast.Walk(tv, &def.Term)
			tv.buf.WriteByte(')')
			tv.sb.WriteByte(')')
			for _, b := range def.Descriptions {
				tv.buf.WriteString(" (DD ")
				tv.sb.WriteString(" (DD ")
				ast.WalkDescriptionSlice(tv, b)
				tv.buf.WriteByte(')')
				tv.sb.WriteByte(')')
			}
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.TableNode:
		tv.buf.WriteString("(TAB")
		tv.sb.WriteString("(TAB")
		if len(n.Header) > 0 {
			tv.buf.WriteString(" (TR")
			tv.sb.WriteString(" (TR")
			for _, cell := range n.Header {
				tv.buf.WriteString(" (TH")
				tv.buf.WriteString(alignString[cell.Align])
				tv.sb.WriteString(" (TH")
				tv.sb.WriteString(alignString[cell.Align])
				ast.Walk(tv, &cell.Inlines)
				tv.buf.WriteString(")")
				tv.sb.WriteString(")")
			}
			tv.buf.WriteString(")")
			tv.sb.WriteString(")")
		}
		if len(n.Rows) > 0 {
			tv.buf.WriteString(" ")
			tv.sb.WriteString(" ")
			for _, row := range n.Rows {
				tv.buf.WriteString("(TR")
				tv.sb.WriteString("(TR")
				for i, cell := range row {
					if i == 0 {
						tv.buf.WriteString(" ")
						tv.sb.WriteString(" ")
					}
					tv.buf.WriteString("(TD")
					tv.buf.WriteString(alignString[cell.Align])
					tv.sb.WriteString("(TD")
					tv.sb.WriteString(alignString[cell.Align])
					ast.Walk(tv, &cell.Inlines)
					tv.buf.WriteString(")")
					tv.sb.WriteString(")")
				}
				tv.buf.WriteString(")")
				tv.sb.WriteString(")")
			}
		}
		tv.buf.WriteString(")")
		tv.sb.WriteString(")")
	case *ast.TranscludeNode:
		fmt.Fprintf(&tv.buf, "(TRANSCLUDE %v)", n.Ref)
		fmt.Fprintf(&tv.sb, "(TRANSCLUDE %v)", n.Ref)
		tv.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		tv.buf.WriteString("(BLOB ")
		tv.buf.WriteString(n.Syntax)
		tv.buf.WriteString(")")
		tv.sb.WriteString("(BLOB ")
		tv.sb.WriteString(n.Syntax)
		tv.sb.WriteString(")")
	case *ast.TextNode:
		tv.buf.WriteString(n.Text)
		tv.sb.WriteString(n.Text)
	case *ast.SpaceNode:
		if l := n.Count(); l == 1 {
			tv.buf.WriteString("SP")
			tv.sb.WriteString("SP")
		} else {
			fmt.Fprintf(&tv.buf, "SP%d", l)
			fmt.Fprintf(&tv.sb, "SP%d", l)
		}
	case *ast.BreakNode:
		if n.Hard {
			tv.buf.WriteString("HB")
			tv.sb.WriteString("HB")
		} else {
			tv.buf.WriteString("SB")
			tv.sb.WriteString("SB")
		}
	case *ast.LinkNode:
		fmt.Fprintf(&tv.buf, "(LINK %v", n.Ref)
		fmt.Fprintf(&tv.sb, "(LINK %v", n.Ref)
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedRefNode:
		fmt.Fprintf(&tv.buf, "(EMBED %v", n.Ref)
		fmt.Fprintf(&tv.sb, "(EMBED %v", n.Ref)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedBLOBNode:
		panic("TODO: zmktest blob")
	case *ast.CiteNode:
		fmt.Fprintf(&tv.buf, "(CITE %s", n.Key)
		fmt.Fprintf(&tv.sb, "(CITE %s", n.Key)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.FootnoteNode:
		tv.buf.WriteString("(FN")
		tv.sb.WriteString("(FN")
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		tv.buf.WriteString("(MARK")
		tv.sb.WriteString("(MARK")
		if n.Mark != "" {
			tv.buf.WriteString(" \"")
			tv.buf.WriteString(n.Mark)
			tv.buf.WriteByte('"')
			tv.sb.WriteString(" \"")
			tv.sb.WriteString(n.Mark)
			tv.sb.WriteByte('"')
		}
		if n.Fragment != "" {
			tv.buf.WriteString(" #")
			tv.buf.WriteString(n.Fragment)
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.FormatNode:
		fmt.Fprintf(&tv.buf, "{%c", mapFormatKind[n.Kind])
		fmt.Fprintf(&tv.sb, "{%c", mapFormatKind[n.Kind])
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte('}')
		tv.sb.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	case *ast.LiteralNode:
		code, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("No element for code %v", n.Kind))
		}
		tv.buf.WriteByte('{')
		tv.buf.WriteRune(code)
		tv.sb.WriteByte('{')
		tv.sb.WriteRune(code)
		if len(n.Content) > 0 {
			tv.buf.WriteByte(' ')
			tv.buf.Write(n.Content)
			tv.sb.WriteByte(' ')
			tv.sb.Write(n.Content)
		}
		tv.buf.WriteByte('}')
		tv.sb.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	default:
		return tv
	}
	return nil
}

1001
1002
1003
1004
1005
1006
1007
1008

1009
1010
1011
1012
1013
1014
1015
1016
1017

1018
1019
1020
1021


1022
1023
1024

1025
1026
1027
1028



1029
1030

1031
1032
1033
1034
1035

1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1000
1001
1002
1003
1004
1005
1006

1007
1008
1009
1010
1011
1012
1013
1014
1015

1016
1017
1018


1019
1020
1021
1022

1023
1024



1025
1026
1027
1028

1029
1030
1031
1032
1033

1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044







-
+








-
+


-
-
+
+


-
+

-
-
-
+
+
+

-
+




-
+










	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
	ast.LiteralMath:    '$',
}

func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) {
	for _, in := range *is {
		tv.buf.WriteByte(' ')
		tv.sb.WriteByte(' ')
		ast.Walk(tv, in)
	}
}

func (tv *TestVisitor) visitAttributes(a attrs.Attributes) {
	if a.IsEmpty() {
		return
	}
	tv.buf.WriteString("[ATTR")
	tv.sb.WriteString("[ATTR")

	for _, k := range a.Keys() {
		tv.buf.WriteByte(' ')
		tv.buf.WriteString(k)
		tv.sb.WriteByte(' ')
		tv.sb.WriteString(k)
		v := a[k]
		if len(v) > 0 {
			tv.buf.WriteByte('=')
			tv.sb.WriteByte('=')
			if quoteString(v) {
				tv.buf.WriteByte('"')
				tv.buf.WriteString(v)
				tv.buf.WriteByte('"')
				tv.sb.WriteByte('"')
				tv.sb.WriteString(v)
				tv.sb.WriteByte('"')
			} else {
				tv.buf.WriteString(v)
				tv.sb.WriteString(v)
			}
		}
	}

	tv.buf.WriteByte(']')
	tv.sb.WriteByte(']')
}

func quoteString(s string) bool {
	for _, ch := range s {
		if ch <= ' ' {
			return true
		}
	}
	return false
}

Changes to query/parser.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
62
63
64
65
66
67
68

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







+







	searchOperatorSuffixChar = '<'
	searchOperatorMatchChar  = '~'

	kwLimit   = "LIMIT"
	kwOffset  = "OFFSET"
	kwOr      = "OR"
	kwOrder   = "ORDER"
	kwPick    = "PICK"
	kwRandom  = "RANDOM"
	kwReverse = "REVERSE"
)

func (ps *parserState) parse(q *Query) *Query {
	inp := ps.inp
	for {
84
85
86
87
88
89
90
91

92
93
94







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

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







-
+



+
+
+
+
+
+
+







				q.terms = append(q.terms, conjTerms{})
			}
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(kwRandom) {
			q = createIfNeeded(q)
			if len(q.order) == 0 {
			if len(q.order) == 0 && q.pick <= 0 {
				q.order = []sortOrder{{"", false}}
			}
			continue
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(kwPick) {
			if s, ok := ps.parsePick(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(kwOrder) {
			if s, ok := ps.parseOrder(q); ok {
				q = s
				continue
			}
119
120
121
122
123
124
125















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







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







			q = ps.parseActions(q)
			break
		}
		q = ps.parseText(q)
	}
	return q
}

func (ps *parserState) parsePick(q *Query) (*Query, bool) {
	num, ok := ps.scanPosInt()
	if !ok {
		return q, false
	}
	q = createIfNeeded(q)
	if q.pick == 0 || q.pick >= num {
		q.pick = num
	}
	if len(q.order) == 1 && q.order[0].isRandom() {
		q.order = nil
	}
	return q, true
}

func (ps *parserState) parseOrder(q *Query) (*Query, bool) {
	reverse := false
	if ps.acceptKwArgs(kwReverse) {
		reverse = true
	}
	word := ps.scanWord()

Changes to query/parser_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
39
40
41
42
43
44
45

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







+







		{`key:a`, `key:a`}, {`key!:a`, `key!:a`},
		{`key>a`, `key>a`}, {`key!>a`, `key!>a`},
		{`key<a`, `key<a`}, {`key!<a`, `key!<a`},
		{`key~a`, `key~a`}, {`key!~a`, `key!~a`},
		{`key1:a key2:b`, `key1:a key2:b`},
		{`key1: key2:b`, `key1: key2:b`},
		{"word key:a", "key:a word"},
		{`PICK 3`, `PICK 3`}, {`PICK 9 PICK 11`, `PICK 9`}, {`PICK 5 RANDOM`, `PICK 5`}, {`RANDOM PICK 7`, `PICK 7`},
		{`RANDOM`, `RANDOM`}, {`RANDOM a`, `a RANDOM`}, {`a RANDOM`, `a RANDOM`},
		{`RANDOM RANDOM a`, `a RANDOM`},
		{`RANDOMRANDOM a`, `RANDOMRANDOM a`}, {`a RANDOMRANDOM`, `a RANDOMRANDOM`},
		{`ORDER`, `ORDER`}, {"ORDER a b", "b ORDER a"}, {"a ORDER", "a ORDER"}, {"ORDER %", "ORDER %"},
		{"ORDER a %", "% ORDER a"},
		{"ORDER REVERSE", "ORDER REVERSE"}, {"ORDER REVERSE a b", "b ORDER REVERSE a"},
		{"a RANDOM ORDER b", "a ORDER b"}, {"a ORDER b RANDOM", "a ORDER b"},

Changes to query/print.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
63
64
65
66
67
68
69

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







+







		for _, name := range maps.Keys(term.mvals) {
			env.printExprValues(name, term.mvals[name])
		}
		if len(term.search) > 0 {
			env.printExprValues("", term.search)
		}
	}
	env.printPosInt(kwPick, q.pick)
	env.printOrder(q.order)
	env.printPosInt(kwOffset, q.offset)
	env.printPosInt(kwLimit, q.limit)
	env.printActions(q.actions)
}

type printEnv struct {
168
169
170
171
172
173
174

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







+







			}
			env.writeString("ANY")
			env.printHumanSelectExprValues(term.search)
			env.space = true
		}
	}

	env.printPosInt(kwPick, q.pick)
	env.printOrder(q.order)
	env.printPosInt(kwOffset, q.offset)
	env.printPosInt(kwLimit, q.limit)
	env.printActions(q.actions)
}

func (pe *printEnv) printHumanSelectExprValues(values []expValue) {

Changes to query/query.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
42
43
44
45
46
47
48


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







+
+







type Query struct {
	// Fields to be used for selecting
	preMatch MetaMatchFunc // Match that must be true
	terms    []conjTerms

	// Allow to create predictable randomness
	seed int

	pick int // Randomly pick elements, <= 0: no pick

	// Fields to be used for sorting
	order  []sortOrder
	offset int // <= 0: no offset
	limit  int // <= 0: no limit

	// Execute specification
142
143
144
145
146
147
148

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







+







		}
		// }
		if len(term.search) > 0 {
			c.terms[i].search = append([]expValue{}, term.search...)
		}
	}
	c.seed = q.seed
	c.pick = q.pick
	if len(q.order) > 0 {
		c.order = append([]sortOrder{}, q.order...)
	}
	c.offset = q.offset
	c.limit = q.limit
	c.actions = q.actions
	return c
374
375
376
377
378
379
380
381
382




383
384
385
386
387
388
389
390









391
392
393
394
395
396
397
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







-
-
+
+
+
+




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







	}
	negatives := retrieveNegatives(negCalls)
	return func(zid id.Zid) bool {
		return positives.Contains(zid) && !negatives.Contains(zid)
	}
}

// Sort applies the sorter to the slice of meta data.
func (q *Query) Sort(metaList []*meta.Meta) []*meta.Meta {
// AfterSearch applies all terms to the metadata list that was searched.
//
// This includes sorting, offset, limit, and picking.
func (q *Query) AfterSearch(metaList []*meta.Meta) []*meta.Meta {
	if len(metaList) == 0 {
		return metaList
	}

	if q == nil || len(q.order) == 0 {
		sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
		if q == nil {
			return metaList
	if q == nil {
		return sortMetaByZid(metaList)
	}

	metaList = q.doPick(metaList)

	if len(q.order) == 0 {
		if q.pick <= 0 {
			metaList = sortMetaByZid(metaList)
		}
	} else if q.order[0].isRandom() {
		metaList = q.sortRandomly(metaList)
	} else {
		sort.Slice(metaList, createSortFunc(q.order, metaList))
	}

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





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







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

+
+
+
+
+
+

-













-
-
+
+



+
+
+
+
+

func (q *Query) setSeed() {
	if q != nil && q.seed <= 0 {
		q.seed = int(rand.Intn(10000) + 1)
	}
}

func (q *Query) doPick(metaList []*meta.Meta) []*meta.Meta {
	pick := q.pick
	if pick <= 0 {
		return metaList
	}
	if limit := q.limit; limit > 0 && limit < pick {
		pick = limit
	}
	if pick >= len(metaList) && len(q.order) == 0 {
		return q.sortRandomly(metaList)
	}
	return q.doPickN(metaList, pick)
}
func (q *Query) doPickN(metaList []*meta.Meta, pick int) []*meta.Meta {
	metaList = sortMetaByZid(metaList)
	q.setSeed()
	rnd := rand.New(rand.NewSource(int64(q.seed)))
	result := make([]*meta.Meta, pick)
	for i := 0; i < pick; i++ {
		n := rnd.Intn(pick - i)
		result[i] = metaList[n]
		metaList[n] = metaList[len(metaList)-i-1]
		metaList[len(metaList)-i-1] = nil
	}
	return result
}

func (q *Query) sortRandomly(metaList []*meta.Meta) []*meta.Meta {
	// Optimization: RANDOM LIMIT n, where n < len(metaList) is essentially a PICK n.
	if limit := q.limit; limit > 0 && limit < len(metaList) {
		return q.doPickN(metaList, limit)
	}

	metaList = sortMetaByZid(metaList)
	q.setSeed()
	sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
	rnd := rand.New(rand.NewSource(int64(q.seed)))
	rnd.Shuffle(
		len(metaList),
		func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] },
	)
	return metaList
}

// Limit returns only s.GetLimit() elements of the given list.
func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta {
	if q == nil {
		return metaList
	}
	if q.limit > 0 && q.limit < len(metaList) {
		return metaList[:q.limit]
	if limit := q.limit; limit > 0 && limit < len(metaList) {
		return metaList[:limit]
	}
	return metaList
}

func sortMetaByZid(metaList []*meta.Meta) []*meta.Meta {
	sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
	return metaList
}

Changes to query/retrieve.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to query/select.go.

1
2

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

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

14
15
16
17
18
19
20

-
+











-







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

package query

import (
	"bytes"
	"fmt"
	"strings"
	"unicode/utf8"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
274
275
276
277
278
279
280
281
282


283
284
285

286
287
288
289
290
291
292
273
274
275
276
277
278
279


280
281
282
283

284
285
286
287
288
289
290
291







-
-
+
+


-
+







			if !hasUpper {
				return zmk
			}
			return strings.ToLower(zmk)
		}
	}
	is := parser.ParseMetadata(zmk)
	var buf bytes.Buffer
	if _, err := textenc.Create().WriteInlines(&buf, &is); err != nil {
	var sb strings.Builder
	if _, err := textenc.Create().WriteInlines(&sb, &is); err != nil {
		return strings.ToLower(zmk)
	}
	return strings.ToLower(buf.String())
	return strings.ToLower(sb.String())
}

func preprocessSet(set []expValue) [][]expValue {
	result := make([][]expValue, 0, len(set))
	for _, elem := range set {
		splitElems := strings.Split(elem.value, ",")
		valueElems := make([]expValue, 0, len(splitElems))

Changes to query/select_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to query/sorter.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to strfun/escape.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
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
48
49
50
51
52
53
54
































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
		}
		io.WriteString(w, s[last:i])
		w.Write(esc)
		last = i + 1
	}
	io.WriteString(w, s[last:])
}

var (
	jsBackslash   = []byte{'\\', '\\'}
	jsDoubleQuote = []byte{'\\', '"'}
	jsNewline     = []byte{'\\', 'n'}
	jsTab         = []byte{'\\', 't'}
	jsCr          = []byte{'\\', 'r'}
	jsUnicode     = []byte{'\\', 'u', '0', '0', '0', '0'}
	jsHex         = []byte("0123456789ABCDEF")
)

// JSONEscape returns the given string as a byte slice, where every non-printable
// rune is made printable.
func JSONEscape(w io.Writer, s string) (int, error) {
	length := 0
	last := 0
	for i, ch := range s {
		var b []byte
		switch ch {
		case '\t':
			b = jsTab
		case '\r':
			b = jsCr
		case '\n':
			b = jsNewline
		case '"':
			b = jsDoubleQuote
		case '\\':
			b = jsBackslash
		default:
			if ch < ' ' {
				b = jsUnicode
				b[2] = '0'
				b[3] = '0'
				b[4] = jsHex[ch>>4]
				b[5] = jsHex[ch&0xF]
			} else {
				continue
			}
		}
		l1, err := io.WriteString(w, s[last:i])
		if err != nil {
			return 0, err
		}
		l2, err := w.Write(b)
		if err != nil {
			return 0, err
		}
		length += l1 + l2
		last = i + 1
	}
	l, err := io.WriteString(w, s[last:])
	if err != nil {
		return 0, err
	}
	return length + l, nil
}

Changes to strfun/set.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to strfun/slugify.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to strfun/slugify_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to strfun/strfun.go.

1
2

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

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

15
16
17
18
19
20
21

-
+












-







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

// Package strfun provides some string functions.
package strfun

import (
	"bytes"
	"strings"
	"unicode/utf8"
)

// Length returns the number of runes in the given string.
func Length(s string) int {
	return utf8.RuneCountInString(s)
32
33
34
35
36
37
38
39

40
41

42
43
44

45
46

47
48
49
50
51
52
53
54
31
32
33
34
35
36
37

38
39

40
41
42

43
44

45
46
47
48
49
50
51
52
53







-
+

-
+


-
+

-
+








		runes = append(runes, r)
	}
	if len(runes) > maxLen {
		runes = runes[:maxLen]
		runes[maxLen-1] = '\u2025'
	}

	var buf bytes.Buffer
	var sb strings.Builder
	for _, r := range runes {
		buf.WriteRune(r)
		sb.WriteRune(r)
	}
	for i := 0; i < maxLen-len(runes); i++ {
		buf.WriteRune(pad)
		sb.WriteRune(pad)
	}
	return buf.String()
	return sb.String()
}

// SplitLines splits the given string into a list of lines.
func SplitLines(s string) []string {
	return strings.FieldsFunc(s, func(r rune) bool {
		return r == '\n' || r == '\r'
	})
}

Changes to strfun/strfun_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to template/mustache.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
24
25
26
27
28
29
30
31


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

31
32
33
34
35
36
37
38
39







-
+
+







import (
	"fmt"
	"io"
	"reflect"
	"regexp"
	"strings"

	"zettelstore.de/c/html"
	"codeberg.org/t73fde/sxhtml"
	"codeberg.org/t73fde/sxpf"
)

// Node represents a node in the parse tree.
// It is either a Tag or a textNode.
type node interface {
	node()
}
595
596
597
598
599
600
601
602

603
604
605
606
607
608
609
596
597
598
599
600
601
602

603
604
605
606
607
608
609
610







-
+







		if err != nil {
			return err
		}
		if val.IsValid() {
			if n.raw {
				fmt.Fprint(w, val.Interface())
			} else {
				html.Escape(w, fmt.Sprint(val.Interface()))
				sxhtml.NewGenerator(nil).WriteHTML(w, sxpf.MakeString(fmt.Sprint(val.Interface())))
			}
		}
	case *sectionNode:
		if err := tmpl.renderSection(w, n, stack); err != nil {
			return err
		}
	case *partialNode:

Changes to template/mustache_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
17
18
19
20
21
22
23

24
25
26
27
28
29
30







-







// The license text is included in the same directory where this file is
// located. See file LICENSE.
//-----------------------------------------------------------------------------

package template_test

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"
	"testing"

	"zettelstore.de/z/template"
)
208
209
210
211
212
213
214
215
216
217



218
219
220
221
222
223
224
207
208
209
210
211
212
213



214
215
216
217
218
219
220
221
222
223







-
-
-
+
+
+







}

func parseString(data string) (*template.Template, error) {
	return template.ParseString(data, nil)
}

func render(tmpl *template.Template, data interface{}) (string, error) {
	var buf bytes.Buffer
	err := tmpl.Render(&buf, data)
	return buf.String(), err
	var sf strings.Builder
	err := tmpl.Render(&sf, data)
	return sf.String(), err
}

func renderString(data string, errMissing bool, value interface{}) (string, error) {
	tmpl, err := parseString(data)
	if err != nil {
		return "", err
	}

Changes to template/spec_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to tests/client/client_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore client 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.
//-----------------------------------------------------------------------------
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
167
168
169
170
171
172
173

174
175
176
177
178
179
180







-







}

func TestGetParsedEvaluatedZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	encodings := []api.EncodingEnum{
		api.EncoderZJSON,
		api.EncoderHTML,
		api.EncoderSexpr,
		api.EncoderText,
	}
	for _, enc := range encodings {
		content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc)
		if err != nil {
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
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







-
-
-
+
+
+










-
-
+
+







	l := rl.List
	if got := len(l); got != limitAll {
		t.Errorf("Expected list of length %d, got %d", limitAll, got)
		t.Error(rl)
		return
	}
	checkListZid(t, l, 0, allUserZid)
	checkListZid(t, l, 1, writerZid)
	checkListZid(t, l, 2, readerZid)
	// checkListZid(t, l, 3, creatorZid)
	// checkListZid(t, l, 1, writerZid)
	// checkListZid(t, l, 2, readerZid)
	checkListZid(t, l, 1, creatorZid)

	rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, ownerZid, rl.ID) {
		return
	}
	l = rl.List
	if got := len(l); got != 1 {
		t.Errorf("Expected list of length 1, got %d", got)
	if got, exp := len(l), 4; got != exp {
		t.Errorf("Expected list of length %d, got %d", exp, got)
		return
	}
	checkListZid(t, l, 0, allUserZid)
}

func TestGetUnlinkedReferences(t *testing.T) {
	t.Parallel()

Changes to tests/client/crud_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore client 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.
//-----------------------------------------------------------------------------

Changes to tests/client/embed_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
88
89
90
91
92
93
94
95

96
97
98
99
100
101
102
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102







-
+







	}

	content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	if exp, got := "<p></p>", string(content); exp != got {
	if exp, got := "", string(content); exp != got {
		t.Errorf("Zettel %q must contain %q, but got %q", abc10Zid, exp, got)
	}
}

func stringHead(s string) string {
	const maxLen = 40
	if len(s) <= maxLen {

Changes to tests/markdown_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
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

-
+















+










-
-
+
+







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

package tests

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/mdenc"
	_ "zettelstore.de/z/encoder/sexprenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zjsonenc"
	_ "zettelstore.de/z/encoder/shtmlenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	_ "zettelstore.de/z/parser/markdown"
	_ "zettelstore.de/z/parser/zettelmark"
)

77
78
79
80
81
82
83
84

85
86
87
88
89


90
91
92
93
94
95
96
78
79
80
81
82
83
84

85
86
87
88


89
90
91
92
93
94
95
96
97







-
+



-
-
+
+







}

func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice {
	return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi)
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	var buf bytes.Buffer
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc).WriteBlocks(&buf, ast)
			buf.Reset()
			encoder.Create(enc).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk)
	var buf bytes.Buffer
126
127
128
129
130
131
132
133

134
135
136
137
138



139
140
141
142
143
127
128
129
130
131
132
133

134
135
136



137
138
139
140
141
142
143
144







-
+


-
-
-
+
+
+





	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, `abc@@<br>@@{="html"}def`},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk)
	var buf bytes.Buffer
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &ast)
		got := buf.String()
		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.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to tests/regression_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
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

-
+












-






+



















-







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

// Package tests provides some higher-level tests.
package tests

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"

	_ "zettelstore.de/z/box/dirbox"
)

var encodings = []api.EncodingEnum{
	api.EncoderHTML,
	api.EncoderSexpr,
	api.EncoderText,
	api.EncoderZJSON,
}

func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) {
	root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind))
	entries, err := os.ReadDir(root)
	if err != nil {
		panic(err)
118
119
120
121
122
123
124
125
126
127



128
129
130
131
132
133
134
117
118
119
120
121
122
123



124
125
126
127
128
129
130
131
132
133







-
-
-
+
+
+







	return u.Path[len(root):]
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
	t.Helper()

	if enc := encoder.Create(enc); enc != nil {
		var buf bytes.Buffer
		enc.WriteMeta(&buf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, buf.String())
		var sf strings.Builder
		enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, sf.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}

func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)

Changes to tools/build.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
1

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

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

35
36
37
38
39
40
41
42

-
+













-



















-
+







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

// Package main provides a command to build and run the software.
package main

import (
	"archive/zip"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"zettelstore.de/z/strfun"
)

var directProxy = []string{"GOPROXY=direct"}

func executeCommand(env []string, name string, arg ...string) (string, error) {
	logCommand("EXEC", env, name, arg)
	var out bytes.Buffer
	var out strings.Builder
	cmd := prepareCommand(env, name, arg, &out)
	err := cmd.Run()
	return out.String(), err
}

func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd {
	if len(env) > 0 {
126
127
128
129
130
131
132





133
134
135
136
137
138
139
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143







+
+
+
+
+







		return err
	}
	if err := checkStaticcheck(); err != nil {
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	if forRelease {
		if err := checkGoVulncheck(); err != nil {
			return err
		}
	}
	return checkFossilExtra()
}

func checkGoTest(pkg string, testParams ...string) error {
	args := []string{"test", pkg}
	args = append(args, testParams...)
204
205
206
207
208
209
210











211
212
213
214
215
216
217
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







+
+
+
+
+
+
+
+
+
+
+







			if len(out2) > 0 {
				fmt.Fprintln(os.Stderr, out2)
			}
		}
	}
	return err
}

func checkGoVulncheck() error {
	out, err := executeCommand(nil, "govulncheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some checks failed")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func findExecStrict(cmd string, forRelease bool) (string, error) {
	path := findExec(cmd)
	if path != "" || !forRelease {
		return path, nil
	}
	return "", errors.New("Command '" + cmd + "' not installed, but required for release")
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







-
+







		fmt.Fprintln(os.Stderr)
	}
	return nil
}

type zsInfo struct {
	cmd          *exec.Cmd
	out          bytes.Buffer
	out          strings.Builder
	adminAddress string
}

func cmdTestAPI() error {
	var err error
	var info zsInfo
	needServer := !addressInUse(":23123")
269
270
271
272
273
274
275

276
277
278
279
280
281
282
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298







+







		"-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
	logCommand("FORK", nil, name, arg)
	cmd := prepareCommand(nil, name, arg, &info.out)
	if !verbose {
		cmd.Stderr = nil
	}
	err := cmd.Start()
	time.Sleep(2 * time.Second)
	for i := 0; i < 100; i++ {
		time.Sleep(time.Millisecond * 100)
		if addressInUse(info.adminAddress) {
			info.cmd = cmd
			return err
		}
	}

Changes to usecase/authenticate.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/context.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
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

-
+
















+






+
+
+







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

package usecase

import (
	"context"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
)

// ZettelContextPort is the interface used by this use case.
type ZettelContextPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)

	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}

// ZettelContextConfig is the interface to allow the usecase to read some config data.
type ZettelContextConfig interface {
	// Get returns a config value that might be user-specific.
	Get(ctx context.Context, m *meta.Meta, key string) string
}
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
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







+
+
+
+
+

















+















+







			break
		}
		result = append(result, m)

		for _, p := range m.ComputedPairsRest() {
			tasks.addPair(ctx, uc.port, p.Key, p.Value, cost, isBackward, isForward)
		}
		if tags, found := m.GetList(api.KeyTags); found {
			for _, tag := range tags {
				tasks.addSameTag(ctx, uc.port, tag, cost)
			}
		}
	}
	return result, nil
}

type ztlCtxTask struct {
	next *ztlCtxTask
	prev *ztlCtxTask
	meta *meta.Meta
	cost int
}

type contextQueue struct {
	seen    id.Set
	first   *ztlCtxTask
	last    *ztlCtxTask
	maxCost int
	limit   int
	tagCost map[string][]*meta.Meta
}

func newQueue(m *meta.Meta, maxCost, limit int) *contextQueue {
	task := &ztlCtxTask{
		next: nil,
		prev: nil,
		meta: m,
		cost: 0,
	}
	result := &contextQueue{
		seen:    id.NewSet(),
		first:   task,
		last:    task,
		maxCost: maxCost,
		limit:   limit,
		tagCost: make(map[string][]*meta.Meta, 1024),
	}
	return result
}

func (zc *contextQueue) addPair(
	ctx context.Context, port ZettelContextPort,
	key, value string,
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
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







-
+



-
+







-




+
-
+
+







func contextCost(key string) int {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 1
	case api.KeySuccessors, api.KeyPredecessor:
		return 2
	}
	return 5
	return 3
}

func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, newCost int, value string) {
	if (zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit() {
	if zc.costMaxed(newCost) {
		return
	}

	zid, err := id.Parse(value)
	if err != nil {
		return
	}

	m, err := port.GetMeta(ctx, zid)
	if err != nil {
		return
	}
	zc.addMeta(m, newCost)

}
func (zc *contextQueue) addMeta(m *meta.Meta, newCost int) {
	task := &ztlCtxTask{next: nil, prev: nil, meta: m, cost: newCost}
	if zc.first == nil {
		zc.first = task
		zc.last = task
		return
	}

189
190
191
192
193
194
195
196




197


198
199


200















































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


216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272








+
+
+
+

+
+
-
-
+
+

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







	}

	// We have not found a task, therefore the new task is the first one
	task.next = zc.first
	zc.first.prev = task
	zc.first = task
}

func (zc *contextQueue) costMaxed(newCost int) bool {
	return (zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit()
}

func (zc *contextQueue) addIDSet(ctx context.Context, port ZettelContextPort, newCost int, value string) {
	elems := meta.ListFromValue(value)
	refCost := referenceCost(newCost, len(elems))
	for _, val := range meta.ListFromValue(value) {
		zc.addID(ctx, port, newCost, val)
	for _, val := range elems {
		zc.addID(ctx, port, refCost, val)
	}
}

func referenceCost(baseCost int, numReferences int) int {
	baseCost++
	if numReferences < 5 {
		return baseCost
	}
	if numReferences < 9 {
		return baseCost * 2
	}
	if numReferences < 17 {
		return baseCost * 3
	}
	if numReferences < 33 {
		return baseCost * 4
	}
	if numReferences < 65 {
		return baseCost * 5
	}
	return baseCost * numReferences / 8
}

func (zc *contextQueue) addSameTag(ctx context.Context, port ZettelContextPort, tag string, baseCost int) {
	tagMetas, found := zc.tagCost[tag]
	if !found {
		q := query.Parse(api.KeyTags + api.SearchOperatorHas + tag)
		ml, err := port.SelectMeta(ctx, q)
		if err != nil {
			return
		}
		tagMetas = ml
		zc.tagCost[tag] = ml
	}
	cost := tagCost(baseCost, len(tagMetas))
	if zc.costMaxed(cost) {
		return
	}
	for _, m := range tagMetas {
		zc.addMeta(m, cost)
	}
}

func tagCost(baseCost, numTags int) int {
	if numTags < 5 {
		return baseCost + numTags
	}
	return (baseCost + 2) * (numTags + 1) / 4
}

func (zc *contextQueue) next() (*meta.Meta, int) {
	if zc.hasLimit() {
		return nil, -1
	}
	for zc.first != nil {

Changes to usecase/create_zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/delete_zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/evaluate.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
65
66
67
68
69
70
71







72
73
74
75
76
77
78







-
-
-
-
-
-
-







	evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns)
	return bns
}

// RunMetadata executes the use case for a metadata value.
func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice {
	is := parser.ParseMetadata(value)
	evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
	return is
}

// RunMetadataNoLink executes the use case for a metadata value, but ignores link and footnote nodes.
func (uc *Evaluate) RunMetadataNoLink(ctx context.Context, value string) ast.InlineSlice {
	is := parser.ParseMetadataNoLink(value)
	evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
	return is
}

// GetMeta retrieves the metadata of a given zettel identifier.
func (uc *Evaluate) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	return uc.getMeta.Run(ctx, zid)

Changes to usecase/get_all_meta.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to usecase/get_meta.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/get_user.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/get_zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/lists.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/order.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to usecase/parse_zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/refresh.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to usecase/rename_zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/unlinked_refs.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to usecase/update_zettel.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to usecase/usecase.go.

1
2

3
4
5
6
7
8
9
10
11
12
1

2
3
4
5
6
7
8
9
10
11
12

-
+










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

// Package usecase provides (business) use cases for the zettelstore.
package usecase

Changes to usecase/version.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
77
78
79
80
81
82
83
84

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

84
85
86
87
88
89
90
91







-
+







		return
	}
	// TODO: must call PrepareHeader somehow
	http.Error(w, text, code)
}

func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error {
	return adapter.WriteBytes(w, buf.Bytes(), contentType)
	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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

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

2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17

-
+








-







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

// Package api provides api handlers for web requests.
package api

import (
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"

Changes to web/adapter/api/get_unlinked_refs.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
1

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

16
17
18

19
20
21
22
23
24
25

26




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

43






44

45
46
47
48
49
50
51

-
+













-



-
+






-
+
-
-
-
-
















-
+
-
-
-
-
-
-
+
-







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

package api

import (
	"bytes"
	"net/http"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
)

// MakeListUnlinkedMetaHandler creates a new HTTP handler for the use case "list unlinked references".
func (a *API) MakeListUnlinkedMetaHandler(
func (a *API) MakeListUnlinkedMetaHandler(getMeta usecase.GetMeta, unlinkedRefs usecase.UnlinkedReferences) http.HandlerFunc {
	getMeta usecase.GetMeta,
	unlinkedRefs usecase.UnlinkedReferences,
	evaluate *usecase.Evaluate,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		ctx := r.Context()
		zm, err := getMeta.Run(ctx, zid)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		que := r.URL.Query()
		phrase := que.Get(api.QueryKeyPhrase)
		if phrase == "" {
			if zmkTitle, found := zm.Get(api.KeyTitle); found {
			if title, found := zm.Get(api.KeyTitle); found {
				isTitle := evaluate.RunMetadata(ctx, zmkTitle)
				encdr := textenc.Create()
				var b strings.Builder
				_, err = encdr.WriteInlines(&b, &isTitle)
				if err == nil {
					phrase = b.String()
				phrase = parser.NormalizedSpacedText(title)
				}
			}
		}

		metaList, err := unlinkedRefs.Run(ctx, phrase, adapter.AddUnlinkedRefsToQuery(adapter.GetQuery(que), zm))
		if err != nil {
			a.reportUsecaseError(w, err)
			return

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to web/adapter/errors.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to web/adapter/request.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to web/adapter/response.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
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

-
+




















-
-
+
+







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

package adapter

import (
	"errors"
	"fmt"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// WriteBytes emits the given data to the response writer.
func WriteBytes(w http.ResponseWriter, data []byte, contentType string) error {
// WriteData emits the given data to the response writer.
func WriteData(w http.ResponseWriter, data []byte, contentType string) error {
	if len(data) == 0 {
		w.WriteHeader(http.StatusNoContent)
		return nil
	}
	PrepareHeader(w, contentType)
	w.WriteHeader(http.StatusOK)
	_, err := w.Write(data)

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
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
47
48
49
50
51
52
53

54
55

56
57

58
59


60







61
62
63
64
65
66
67
68







-
+

-
+

-
+

-
-
+
-
-
-
-
-
-
-
+







			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "Copy Zettel", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "Versionzettel", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "Folgezettel", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			m := origZettel.Meta
			title := parser.ParseMetadata(m.GetTitle())
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			textTitle, err2 := encodeInlinesText(&title, wui.gentext)
			if err2 != nil {
				wui.reportError(ctx, w, err2)
				return
			}
			htmlTitle := wui.getSimpleHTMLEncoder().InlinesString(&title)
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), textTitle, htmlTitle, "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), title, "", roleData, syntaxData)
		}
	}
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
	roleData := dataListFromArrangement(ucListRoles.Run(ctx))
	syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx))
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
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







-
+









-
+







	return nil
}

func (wui *WebUI) renderZettelForm(
	ctx context.Context,
	w http.ResponseWriter,
	zettel domain.Zettel,
	title, heading string,
	title string,
	formActionURL string,
	roleData []string,
	syntaxData []string,
) {
	user := server.GetUser(ctx)
	m := zettel.Meta
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), title, "", user, &base)
	wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
		Heading:       heading,
		Heading:       title,
		FormActionURL: formActionURL,
		MetaTitle:     m.GetDefault(api.KeyTitle, ""),
		MetaTags:      m.GetDefault(api.KeyTags, ""),
		MetaRole:      m.GetDefault(api.KeyRole, ""),
		HasRoleData:   len(roleData) > 0,
		RoleData:      roleData,
		HasSyntaxData: len(syntaxData) > 0,
172
173
174
175
176
177
178
179

180
181
165
166
167
168
169
170
171

172
173
174







-
+


		m.Set(api.KeyTitle, q.Human())
		m.Set(api.KeySyntax, api.ValueSyntaxZmk)
		if qval := q.String(); qval != "" {
			m.Set(api.KeyQuery, qval)
		}
		zettel := domain.Zettel{Meta: m, Content: domain.NewContent(zmkContent.Bytes())}
		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", "Zettel From List", wui.createNewURL, roleData, syntaxData)
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData)
	}
}

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
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
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







-
+
-
-
-
-




















-
+







	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(
func (wui *WebUI) MakeGetDeleteZettelHandler(getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta) http.HandlerFunc {
	getMeta usecase.GetMeta,
	getAllMeta usecase.GetAllMeta,
	evaluate *usecase.Evaluate,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		ms, err := getAllMeta.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := ms[0]

		var shadowedBox string
		var incomingLinks simpleLinks
		if len(ms) > 1 {
			shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???")
		} else {
			getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate))
			getTextTitle := wui.makeGetTextTitle(ctx, getMeta)
			incomingLinks = wui.encodeIncoming(m, getTextTitle)
		}
		uselessFiles := retrieveUselessFiles(m)

		user := server.GetUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Delete Zettel "+m.Zid.String(), "", user, &base)

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
34
35
36
37
38
39
40
41

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

41
42
43
44
45
46
47
48







-
+







		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "Edit Zettel", "", roleData, syntaxData)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData)
	}
}

// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
33
34
35
36
37
38
39
40

41
42
43
33
34
35
36
37
38
39

40
41
42
43







-
+



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

		err = adapter.WriteBytes(w, data, "")
		err = adapter.WriteData(w, data, "")
		wui.log.IfErr(err).Msg("Write favicon")
	}
}

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to web/adapter/webui/get_info.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
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

-
+











-





+







+







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

package webui

import (
	"bytes"
	"context"
	"net/http"
	"sort"
	"strings"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
)

type metaDataInfo struct {
	Key   string
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
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







-



-
+

-
-
-
+
+
+




-
-
+
+
+
+









-
+


-
+








		zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		evalMetadata := createEvalMetadataFunc(ctx, evaluate)
		enc := wui.getSimpleHTMLEncoder()
		pairs := zn.Meta.ComputedPairs()
		metaData := make([]metaDataInfo, len(pairs))
		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata)
		getTextTitle := wui.makeGetTextTitle(ctx, getMeta)
		for i, p := range pairs {
			var buf bytes.Buffer
			wui.writeHTMLMetaValue(
				&buf, p.Key, p.Value,
			var sb strings.Builder
			if sval := wui.writeHTMLMetaValue(
				p.Key, p.Value,
				getTextTitle,
				func(val string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, val)
				},
				enc)
			metaData[i] = metaDataInfo{p.Key, buf.String()}
				enc); !sxpf.IsNil(sval) {
				wui.htmlGen.WriteHTML(&sb, sval)
			}
			metaData[i] = metaDataInfo{p.Key, sb.String()}
		}
		summary := collect.References(zn)
		locLinks, qLinks, extLinks := splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))
		queryLinks := make([]simpleLink, len(qLinks))
		for i, sq := range qLinks {
			queryLinks[i].Text = sq
			queryLinks[i].URL = wui.NewURLBuilder('h').AppendQuery(sq).String()
		}

		textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext)
		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = textTitle
			phrase = title
		}
		phrase = strings.TrimSpace(phrase)
		unlinkedMeta, err := unlinkedRefs.Run(
			ctx, phrase, adapter.AddUnlinkedRefsToQuery(nil, zn.InhMeta))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
112
113
114
115
116
117
118
119

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

121
122
123
124
125
126
127
128







-
+







			endnotes = ""
		}

		user := server.GetUser(ctx)
		canCreate := wui.canCreate(ctx, user)
		apiZid := api.ZettelID(zid.String())
		var base baseData
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, "", user, &base)
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, "", user, &base)
		wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct {
			Zid            string
			WebURL         string
			ContextURL     string
			CanWrite       bool
			EditURL        string
			CanCopy        bool

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

1
2

3
4
5
6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
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

-
+











-

+


-
-



-
+







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

package webui

import (
	"bytes"
	"net/http"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getMeta usecase.GetMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
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
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







-
+
-
-
-

















-
-
+
+




+

+
-
-
+
+

-
+


















-







-
+


















-










-
+




-
+


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

-
+

-
+

















-
-
-
+
+
+
+







		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))

		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.createZettelEncoder(ctx, zn.InhMeta)
		enc := wui.getSimpleHTMLEncoder()
		evalMetadata := createEvalMetadataFunc(ctx, evaluate)
		textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext)
		htmlTitle := encodeEvaluatedTitleHTML(zn.InhMeta, evalMetadata, enc)
		htmlContent, err := enc.BlocksString(&zn.Ast)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		var roleCSSURL string
		cssZid, err := wui.retrieveCSSZidFromRole(ctx, *zn.InhMeta)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if cssZid != id.Invalid {
			roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String()
		}
		user := server.GetUser(ctx)
		roleText := zn.Meta.GetDefault(api.KeyRole, "")
		canCreate := wui.canCreate(ctx, user)
		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata)
		extURL, hasExtURL := formatURLFromMeta(zn.Meta, api.KeyURL)
		getTextTitle := wui.makeGetTextTitle(ctx, getMeta)
		extURL, hasExtURL := wui.formatURLFromMeta(zn.Meta, api.KeyURL)
		folgeLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle))
		backLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle))
		successorLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeySuccessors, getTextTitle))
		apiZid := api.ZettelID(zid.String())

		var base baseData
		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, roleCSSURL, user, &base)
		base.MetaHeader = enc.MetaString(zn.InhMeta, evalMetadata)
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, roleCSSURL, user, &base)
		base.MetaHeader = enc.MetaString(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
			HTMLTitle       string
			Heading         string
			RoleCSS         string
			CanWrite        bool
			EditURL         string
			Zid             string
			InfoURL         string
			RoleText        string
			RoleURL         string
			Tags            simpleLinks
			CanCopy         bool
			CopyURL         string
			CanVersion      bool
			VersionURL      string
			CanFolge        bool
			FolgeURL        string
			PredecessorRefs string
			PrecursorRefs   string
			HasExtURL       bool
			ExtURL          string
			ExtNewWindow    string
			Author          string
			Content         string
			NeedBottomNav   bool
			FolgeLinks      simpleLinks
			BackLinks       simpleLinks
			SuccessorLinks  simpleLinks
		}{
			HTMLTitle:       htmlTitle,
			Heading:         title,
			RoleCSS:         roleCSSURL,
			CanWrite:        wui.canWrite(ctx, user, zn.Meta, zn.Content),
			EditURL:         wui.NewURLBuilder('e').SetZid(apiZid).String(),
			Zid:             zid.String(),
			InfoURL:         wui.NewURLBuilder('i').SetZid(apiZid).String(),
			RoleText:        roleText,
			RoleURL:         wui.NewURLBuilder('h').AppendQuery(api.KeyRole + api.SearchOperatorHas + roleText).String(),
			Tags:            createSimpleLinks(wui.buildTagInfos(zn.Meta)),
			CanCopy:         canCreate && !zn.Content.IsBinary(),
			CopyURL:         wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(),
			CanVersion:      canCreate,
			VersionURL:      wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String(),
			CanFolge:        canCreate,
			FolgeURL:        wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(),
			PredecessorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPredecessor, getTextTitle),
			PrecursorRefs:   wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle),
			ExtURL:          extURL,
			HasExtURL:       hasExtURL,
			ExtNewWindow:    htmlAttrNewWindow(hasExtURL),
			Author:          zn.Meta.GetDefault(api.KeyAuthor, ""),
			Content:         htmlContent,
			NeedBottomNav:   folgeLinks.Has || backLinks.Has || successorLinks.Has,
			FolgeLinks:      folgeLinks,
			BackLinks:       backLinks,
			SuccessorLinks:  successorLinks,
		})
	}
}

func formatURLFromMeta(m *meta.Meta, key string) (string, bool) {
func (wui *WebUI) formatURLFromMeta(m *meta.Meta, key string) (string, bool) {
	val, found := m.Get(key)
	if !found {
		return "", false
	}
	if found && val == "" {
	if val == "" {
		return "", false
	}
	var buf bytes.Buffer
	_, err := html.AttributeEscape(&buf, val)
	if err != nil {
		return "", false
	}

	return buf.String(), true
}

	var sb strings.Builder
func encodeInlinesText(is *ast.InlineSlice, enc *textenc.Encoder) (string, error) {
	if is == nil || len(*is) == 0 {
		return "", nil
	}

	_, err := wui.htmlGen.WriteHTML(&sb, wui.transformURL(val))
	var buf bytes.Buffer
	_, err := enc.WriteInlines(&buf, is)
	if err != nil {
		return "", err
		return "", false
	}
	return buf.String(), nil
	return sb.String(), true
}

func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink {
	var tagInfos []simpleLink
	if tags, ok := m.GetList(api.KeyTags); ok {
		ub := wui.NewURLBuilder('h')
		tagInfos = make([]simpleLink, len(tags))
		for i, tag := range tags {
			tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery(api.KeyTags + api.SearchOperatorHas + tag).String()}
			ub.ClearQuery()
		}
	}
	return tagInfos
}

func (wui *WebUI) encodeIdentifierSet(m *meta.Meta, key string, getTextTitle getTextTitleFunc) string {
	if value, ok := m.Get(key); ok {
		var buf bytes.Buffer
		wui.writeIdentifierSet(&buf, meta.ListFromValue(value), getTextTitle)
		return buf.String()
		sval := wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
		var sb strings.Builder
		wui.htmlGen.WriteHTML(&sb, sval)
		return sb.String()
	}
	return ""
}

func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink {
	values, ok := m.GetList(key)
	if !ok || len(values) == 0 {

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to web/adapter/webui/htmlgen.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
1

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

14
15
16
17
18
19

20
21
22
23
24
25
26
27


28
29
30
31
32
33
34
35
36
37



38
39
40

41
42









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













126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215


216
217

218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233



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

250


251
252




253
254
255
256
257
258
259
260
261
262
263


264



265
266


267
268


269
270
271
272

273
274

275
276

277
278
279
280
281
282
283
284
285
286
287
288


289
290



291






292
293
294




295
296
297
298
299



300
301








302
303





304
305
306
307
308
309
310




311

312
313
314

315
316
317
318


319
320


321






322








323



324
325













326

327

-
+











-
+


+


-
+
+

+




-
-










-
-
-
+
+
+
-


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








+
+
+
+
+
+

-
-
+

-
+



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


+
-
+
-
-
+

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

-
-
+
+


-
+

-
+

-
+
+
+
+
+







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

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

-
+


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

//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

package webui

import (
	"bytes"
	"net/url"
	"strings"

	"codeberg.org/t73fde/sxhtml"
	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/maps"
	"zettelstore.de/c/sexpr"
	"zettelstore.de/c/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/sexprenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	builder   urlBuilder
	textEnc   *textenc.Encoder
	extMarker string
	tx    *sexprenc.Transformer
	th    *shtml.Transformer
	symAt *sxpf.Symbol
	env       *html.EncEnvironment
}

func createGenerator(builder urlBuilder, extMarker string) *htmlGenerator {
	env := html.NewEncEnvironment(nil, 1)
	gen := &htmlGenerator{
		builder:   builder,
		textEnc:   textenc.Create(),
		extMarker: extMarker,
		env:       env,
	}

func (wui *WebUI) createGenerator(builder urlBuilder) *htmlGenerator {
	th := shtml.NewTransformer(1, wui.sf)
	symA := th.Make("a")
	symImg := th.Make("img")
	symAt := th.Make(sxhtml.NameSymAttr)

	symHref := th.Make("href")
	symClass := th.Make("class")
	symTarget := th.Make("target")
	symRel := th.Make("rel")

	findA := func(obj sxpf.Object) (attr, assoc, rest *sxpf.List) {
		lst, ok := obj.(*sxpf.List)
		if !ok || !symA.IsEqual(lst.Car()) {
			return nil, nil, nil
		}
		rest = lst.Tail()
		if rest == nil {
			return nil, nil, nil
		}
		objA := rest.Car()
		attr, ok = objA.(*sxpf.List)
		if !ok || !symAt.IsEqual(attr.Car()) {
			return nil, nil, nil
		}
		return attr, attr.Tail(), rest.Tail()
	}
	linkZettel := func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object {
		obj, err := prevFn.Call(nil, args)
		if err != nil {
			return sxpf.Nil()
		}
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}

		hrefP := assoc.Assoc(symHref)
		if hrefP == nil {
			return obj
		}
		href, ok := hrefP.Cdr().(sxpf.String)
		if !ok {
			return obj
		}
		zid, fragment, hasFragment := strings.Cut(href.String(), "#")
		u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
		assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String())))
		return rest.Cons(assoc.Cons(symAt)).Cons(symA)
	}

	th.SetRebinder(func(te *shtml.TransformEnv) {
		te.Rebind(sexpr.NameSymLinkZettel, linkZettel)
		te.Rebind(sexpr.NameSymLinkFound, linkZettel)
		te.Rebind(sexpr.NameSymLinkBased, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object {
			obj, err := prevFn.Call(nil, args)
			if err != nil {
				return sxpf.Nil()
			}
			attr, assoc, rest := findA(obj)
			if attr == nil {
				return obj
			}
			hrefP := assoc.Assoc(symHref)
			if hrefP == nil {
				return obj
			}
			href, ok := hrefP.Cdr().(sxpf.String)
			if !ok {
				return obj
			}
			u := builder.NewURLBuilder('/').SetRawLocal(href.String())
			assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String())))
			return rest.Cons(assoc.Cons(symAt)).Cons(symA)
		})
		te.Rebind(sexpr.NameSymLinkQuery, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object {
			obj, err := prevFn.Call(nil, args)
			if err != nil {
				return sxpf.Nil()
			}
	env.Builtins.Set(sexpr.SymLinkZettel, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel))
	env.Builtins.Set(sexpr.SymLinkFound, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel))
	env.Builtins.Set(sexpr.SymLinkBased, sxpf.NewBuiltin("linkB", true, 2, -1, gen.generateLinkBased))
	env.Builtins.Set(sexpr.SymLinkQuery, sxpf.NewBuiltin("linkQ", true, 2, -1, gen.generateLinkQuery))
	env.Builtins.Set(sexpr.SymLinkExternal, sxpf.NewBuiltin("linkE", true, 2, -1, gen.generateLinkExternal))

	f, err := env.Builtins.LookupForm(sexpr.SymEmbed)
	if err != nil {
		panic(err)
	}
	b := f.(*sxpf.Builtin)
	env.Builtins.Set(sexpr.SymEmbed, sxpf.NewBuiltin(b.Name(), true, 3, -1, gen.makeGenerateEmbed(b.GetValue())))
	return gen
			attr, assoc, rest := findA(obj)
			if attr == nil {
				return obj
			}
			hrefP := assoc.Assoc(symHref)
			if hrefP == nil {
				return obj
			}
			href, ok := hrefP.Cdr().(sxpf.String)
			if !ok {
				return obj
			}
			ur, err := url.Parse(href.String())
			if err != nil {
				return obj
			}
			q := ur.Query().Get(api.QueryKeyQuery)
			if q == "" {
				return obj
			}
			u := builder.NewURLBuilder('h').AppendQuery(q)
			assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String())))
			return rest.Cons(assoc.Cons(symAt)).Cons(symA)
		})
		te.Rebind(sexpr.NameSymLinkExternal, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object {
			obj, err := prevFn.Call(nil, args)
			if err != nil {
				return sxpf.Nil()
			}
			attr, assoc, rest := findA(obj)
			if attr == nil {
				return obj
			}
			assoc = assoc.Cons(sxpf.Cons(symClass, sxpf.MakeString("external"))).
				Cons(sxpf.Cons(symTarget, sxpf.MakeString("_blank"))).
				Cons(sxpf.Cons(symRel, sxpf.MakeString("noopener noreferrer")))
			return rest.Cons(assoc.Cons(symAt)).Cons(symA)
		})
		te.Rebind(sexpr.NameSymEmbed, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object {
			obj, err := prevFn.Call(nil, args)
			if err != nil {
				return sxpf.Nil()
			}
			lst, ok := obj.(*sxpf.List)
			if !ok || !symImg.IsEqual(lst.Car()) {
				return obj
			}
			attr, ok := lst.Tail().Car().(*sxpf.List)
			if !ok || !symAt.IsEqual(attr.Car()) {
				return obj
			}
			symSrc := th.Make("src")
			srcP := attr.Tail().Assoc(symSrc)
			if srcP == nil {
				return obj
			}
			src, ok := srcP.Cdr().(sxpf.String)
			if !ok {
				return obj
			}
			zid := api.ZettelID(src)
			if !zid.IsValid() {
				return obj
			}
			u := builder.NewURLBuilder('z').SetZid(zid)
			imgAttr := attr.Tail().Cons(sxpf.Cons(symSrc, sxpf.MakeString(u.String()))).Cons(symAt)
			return lst.Tail().Tail().Cons(imgAttr).Cons(symImg)
		})
	})

	return &htmlGenerator{
		tx:    sexprenc.NewTransformer(),
		th:    th,
		symAt: symAt,
	}
}

var mapMetaKey = map[string]string{
	api.KeyCopyright: "copyright",
	api.KeyLicense:   "license",
}

func (g *htmlGenerator) MetaString(m *meta.Meta, evalMeta encoder.EvalMetaFunc) string {
	tm := g.tx.GetMeta(m, evalMeta)
	hm, err := g.th.Transform(tm)
	if err != nil {
		return ""
	}

	ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
	var buf bytes.Buffer

	metaMap := make(map[string]*sxpf.List, m.Length())
	if tags, ok := m.Get(api.KeyTags); ok {
		writeMetaTags(&buf, tags)
		metaMap[api.KeyTags] = g.transformMetaTags(tags)
		ignore.Set(api.KeyTags)
	}

	for elem := hm; elem != nil; elem = elem.Tail() {
		mlst, ok := elem.Car().(*sxpf.List)
		if !ok {
			continue
		}
		att, ok := mlst.Tail().Car().(*sxpf.List)
		if !ok {
			continue
		}
		if !att.Car().IsEqual(g.symAt) {
			continue
		}
	for _, p := range m.ComputedPairs() {
		key := p.Key
		if ignore.Has(key) {
		a := make(attrs.Attributes, 32)
		for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() {
			if p, ok2 := aelem.Car().(*sxpf.List); ok2 {
				key := p.Car()
				val := p.Cdr()
				if tail, ok3 := val.(*sxpf.List); ok3 {
					val = tail.Car()
				}
				a = a.Set(key.String(), val.String())
			}
		}
		name, found := a.Get("name")
		if !found || ignore.Has(name) {
			continue
		}
		var newName string
		if altKey, found := mapMetaKey[key]; found {
		if altName, found2 := mapMetaKey[name]; found2 {
			buf.WriteString(`<meta name="`)
			buf.WriteString(altKey)
			newName = altName
		} else {
			buf.WriteString(`<meta name="zs-`)
			buf.WriteString(key)
		}
		buf.WriteString(`" content="`)
			newName = "zs-" + name
		}
		a = a.Set("name", newName)
		metaMap[newName] = g.th.TransformMeta(a)
	}
	result := sxpf.Nil()
	keys := maps.Keys(metaMap)
	for i := len(keys) - 1; i >= 0; i-- {
		result = result.Cons(metaMap[keys[i]])
	}

		is := evalMeta(p.Value)
		var sb strings.Builder
	var sb strings.Builder
		g.textEnc.WriteInlines(&sb, &is)
		html.AttributeEscape(&buf, sb.String())
		buf.WriteString("\">\n")
	gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(result))
	_, _ = gen.WriteListHTML(&sb, result)
	}
	return buf.String()
	return sb.String()
}
func writeMetaTags(buf *bytes.Buffer, tags string) {
	buf.WriteString(`<meta name="keywords" content="`)
func (g *htmlGenerator) transformMetaTags(tags string) *sxpf.List {
	var sb strings.Builder
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			buf.WriteString(", ")
			sb.WriteString(", ")
		}
		html.AttributeEscape(buf, strings.TrimPrefix(val, "#"))
		sb.WriteString(strings.TrimPrefix(val, "#"))
	}
	buf.WriteString("\">\n")
	metaTags := sb.String()
	if len(metaTags) == 0 {
		return sxpf.Nil()
	}
	return g.th.TransformMeta(attrs.Attributes{"name": "keywords", "content": metaTags})
}

// BlocksString encodes a block slice.
func (g *htmlGenerator) BlocksString(bs *ast.BlockSlice) (string, error) {
	if bs == nil || len(*bs) == 0 {
		return "", nil
	}
	lst := sexprenc.GetSexpr(bs)
	var buf bytes.Buffer
	sx := g.tx.GetSexpr(bs)
	sh, err := g.th.Transform(sx)
	g.env.ReplaceWriter(&buf)
	sxpf.Eval(g.env, lst)
	if g.env.GetError() == nil {
	if err != nil {
		g.env.WriteEndnotes()
	}
	g.env.ReplaceWriter(nil)
	return buf.String(), g.env.GetError()
}

		return "", err
	}
	var sb strings.Builder
// InlinesString writes an inline slice to the writer
func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) string {
	if is == nil || len(*is) == 0 {
		return ""
	gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(sh), sxhtml.WithNewline)
	_, err = gen.WriteListHTML(&sb, sh)
	if err != nil {
		return sb.String(), err
	}
	return html.EvaluateInline(g.env, sexprenc.GetSexpr(is), true, false)
}


	_, err = gen.WriteHTML(&sb, g.th.Endnotes())
func (g *htmlGenerator) generateLinkZettel(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		zid, fragment, hasFragment := strings.Cut(refValue, "#")
		u := g.builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
	return sb.String(), err
}
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	}
	return nil, nil
}


// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sxpf.List {
	if is == nil || len(*is) == 0 {
		return sxpf.Nil()
	}
	sx := g.tx.GetSexpr(is)
func (g *htmlGenerator) generateLinkBased(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		u := g.builder.NewURLBuilder('/').SetRawLocal(refValue)
	sh, err := g.th.Transform(sx)
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	if err != nil {
		return sxpf.Nil()
	}
	return nil, nil
	return sh
}

// InlinesString returns an inline slice, encoded as a HTML string.
func (g *htmlGenerator) generateLinkQuery(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) string {
	if sh := g.InlinesSxHTML(is); !sxpf.IsNil(sh) {
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		queryExpr := query.Parse(refValue).String()
		var sb strings.Builder
		u := g.builder.NewURLBuilder('h').AppendQuery(queryExpr)
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	}
	return nil, nil
}

		gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(sh))
func (g *htmlGenerator) generateLinkExternal(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		a = a.Set("href", refValue).
			AddClass("external").
			Set("target", "_blank").
			Set("rel", "noopener noreferrer")
		html.WriteLink(env, args, a, refValue, g.extMarker)
		_, _ = gen.WriteListHTML(&sb, sh)
	}
	return nil, nil
}
		return sb.String()
	}

func (g *htmlGenerator) makeGenerateEmbed(oldFn sxpf.BuiltinFn) sxpf.BuiltinFn {
	return func(senv sxpf.Environment, args *sxpf.Pair, arity int) (sxpf.Value, error) {
		env := senv.(*html.EncEnvironment)
		ref := env.GetPair(args.GetTail())
		refValue := env.GetString(ref.GetTail())
		zid := api.ZettelID(refValue)
		if !zid.IsValid() {
			return oldFn(senv, args, arity)
		}
		u := g.builder.NewURLBuilder('z').SetZid(zid)
		env.WriteImageWithSource(args, u.String())
		return nil, nil
	return ""
	}
}

Changes to web/adapter/webui/htmlmeta.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
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

-
+














-



+
+

-




+



-
-

-




-
+
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+


-
+


-
+

-
+

-
+

-
+

-
-
+

+


-
-
-
-
+
+


-
-
+





+
-
+
-
-
+
-

+
+

-
-
-
+
+
+



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

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


-
+
+

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

+


-
-
+
+


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

+


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










-
+

-
+






-
+



-
+

-
+

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

package webui

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

	"codeberg.org/t73fde/sxhtml"
	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
)

var space = []byte{' '}

func (wui *WebUI) writeHTMLMetaValue(
	w io.Writer,
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
) {
) sxpf.Object {
	var sval sxpf.Object = sxpf.Nil()
	switch kt := meta.Type(key); kt {
	case meta.TypeCredential:
		writeCredential(w, value)
		sval = sxpf.MakeString(value)
	case meta.TypeEmpty:
		writeEmpty(w, value)
		sval = sxpf.MakeString(value)
	case meta.TypeID:
		wui.writeIdentifier(w, value, getTextTitle)
		sval = wui.transformIdentifier(value, getTextTitle)
	case meta.TypeIDSet:
		wui.writeIdentifierSet(w, meta.ListFromValue(value), getTextTitle)
		sval = wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
	case meta.TypeNumber:
		wui.writeNumber(w, key, value)
		sval = wui.transformLink(key, value, value)
	case meta.TypeString:
		writeString(w, value)
		sval = sxpf.MakeString(value)
	case meta.TypeTagSet:
		wui.writeTagSet(w, key, meta.ListFromValue(value))
		sval = wui.transformTagSet(key, meta.ListFromValue(value))
	case meta.TypeTimestamp:
		if ts, ok := meta.TimeValue(value); ok {
			writeTimestamp(w, ts)
			sval = wui.transformTimestamp(ts)
		}
	case meta.TypeURL:
		writeURL(w, value)
		sval = wui.transformURL(value)
	case meta.TypeWord:
		wui.writeWord(w, key, value)
		sval = wui.transformWord(key, value)
	case meta.TypeWordSet:
		wui.writeWordSet(w, key, meta.ListFromValue(value))
		sval = wui.transformWordSet(key, meta.ListFromValue(value))
	case meta.TypeZettelmarkup:
		io.WriteString(w, encodeZmkMetadata(value, evalMetadata, gen))
		sval = wui.transformZmkMetadata(value, evalMetadata, gen)
	default:
		html.Escape(w, value)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
		sval = sxpf.Nil().Cons(sxpf.MakeString(fmt.Sprintf(" <b>(Unhandled type: %v, key: %v)</b>", kt, key))).Cons(wui.sf.MustMake("b"))
	}
	return sval
}

func writeCredential(w io.Writer, val string) { html.Escape(w, val) }
func writeEmpty(w io.Writer, val string)      { html.Escape(w, val) }

func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTextTitle getTextTitleFunc) {
func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sxpf.Object {
	text := sxpf.MakeString(val)
	zid, err := id.Parse(val)
	if err != nil {
		html.Escape(w, val)
		return
		return text
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
		attrs := sxpf.Nil()
		if title == "" {
		if title != "" {
			fmt.Fprintf(w, "<a href=\"%v\">%v</a>", ub, zid)
		} else {
			attrs = attrs.Cons(sxpf.Cons(wui.sf.MustMake("title"), sxpf.MakeString(title)))
			fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", ub, title, zid)
		}
		attrs = attrs.Cons(sxpf.Cons(wui.sf.MustMake("href"), sxpf.MakeString(ub.String()))).Cons(wui.sf.MustMake(sxhtml.NameSymAttr))
		return sxpf.Nil().Cons(sxpf.MakeString(zid.String())).Cons(attrs).Cons(wui.sf.MustMake("a"))
	case found == 0:
		fmt.Fprintf(w, "<s>%v</s>", val)
	case found < 0:
		io.WriteString(w, val)
		return sxpf.Nil().Cons(text).Cons(wui.sf.MustMake("s"))
	default: // case found < 0:
		return text
	}
}

func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTextTitle getTextTitleFunc) {
func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) sxpf.Object {
	for i, val := range vals {
		if i > 0 {
			w.Write(space)
		}
		wui.writeIdentifier(w, val, getTextTitle)
	if len(vals) == 0 {
		return sxpf.Nil()
	}
	space := sxpf.MakeString(" ")
	text := make([]sxpf.Object, 0, 2*len(vals))
	for _, val := range vals {
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
	}
}

	return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span"))
}
func (wui *WebUI) writeNumber(w io.Writer, key, val string) {
	wui.writeLink(w, key, val, val)
}

func writeString(w io.Writer, val string) { html.Escape(w, val) }


func (wui *WebUI) transformTagSet(key string, tags []string) sxpf.Object {
	if len(tags) == 0 {
		return sxpf.Nil()
	}
	space := sxpf.MakeString(" ")
	text := make([]sxpf.Object, 0, 2*len(tags))
func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) {
	for i, tag := range tags {
	for _, tag := range tags {
		if i > 0 {
			w.Write(space)
		}
		text = append(text, space, wui.transformLink(key, tag, tag))
	}
		wui.writeLink(w, key, tag, tag)
	}
}

	return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span"))
}

func (wui *WebUI) transformTimestamp(ts time.Time) sxpf.Object {
func writeTimestamp(w io.Writer, ts time.Time) {
	io.WriteString(w, `<time datetime="`)
	io.WriteString(w, ts.Format("2006-01-02T15:04:05"))
	return sxpf.MakeList(
		wui.sf.MustMake("time"),
		sxpf.MakeList(
			wui.sf.MustMake(sxhtml.NameSymAttr),
			sxpf.Cons(wui.sf.MustMake("datetime"), sxpf.MakeString(ts.Format("2006-01-02T15:04:05"))),
	io.WriteString(w, `">`)
	io.WriteString(w, ts.Format("2006-01-02&nbsp;15:04:05"))
		),
		sxpf.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sxpf.MakeString(ts.Format("2006-01-02&nbsp;15:04:05"))),
	io.WriteString(w, `</time>`)
	)
}

func writeURL(w io.Writer, val string) {
func (wui *WebUI) transformURL(val string) sxpf.Object {
	text := sxpf.MakeString(val)
	u, err := url.Parse(val)
	if err != nil {
	if err == nil {
		html.Escape(w, val)
		return
	}
		if us := u.String(); us != "" {
			return sxpf.MakeList(
				wui.sf.MustMake("a"),
				sxpf.MakeList(
					wui.sf.MustMake(sxhtml.NameSymAttr),
					sxpf.Cons(wui.sf.MustMake("href"), sxpf.MakeString(val)),
					sxpf.Cons(wui.sf.MustMake("target"), sxpf.MakeString("_blank")),
					sxpf.Cons(wui.sf.MustMake("rel"), sxpf.MakeString("noopener noreferrer")),
				),
				text,
			)
		}
	if us := u.String(); us != "" {
		io.WriteString(w, `<a href="`)
		html.AttributeEscape(w, us)
		io.WriteString(w, `" target="_blank" rel="noopener noreferrer">`)
		html.Escape(w, val)
		io.WriteString(w, "</a>")
	}
	return text
}

func (wui *WebUI) writeWord(w io.Writer, key, word string) {
	wui.writeLink(w, key, word, word)
func (wui *WebUI) transformWord(key, word string) sxpf.Object {
	return wui.transformLink(key, word, word)
}

func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) {
	for i, word := range words {
		if i > 0 {
func (wui *WebUI) transformWordSet(key string, words []string) sxpf.Object {
	if len(words) == 0 {
		return sxpf.Nil()
			w.Write(space)
		}
		wui.writeWord(w, key, word)
	}
	space := sxpf.MakeString(" ")
	text := make([]sxpf.Object, 0, 2*len(words))
	for _, tag := range words {
		text = append(text, space, wui.transformWord(key, tag))
	}
	return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span"))
}

func (wui *WebUI) writeLink(w io.Writer, key, value, text string) {
	fmt.Fprintf(w, `<a href="%v">`, wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value))
func (wui *WebUI) transformLink(key, value, text string) sxpf.Object {
	return sxpf.MakeList(
		wui.sf.MustMake("a"),
		sxpf.MakeList(
			wui.sf.MustMake(sxhtml.NameSymAttr),
			sxpf.Cons(wui.sf.MustMake("href"), sxpf.MakeString(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())),
	html.Escape(w, text)
	io.WriteString(w, "</a>")
		),
		sxpf.MakeString(text),
}

	)
type getMetadataFunc func(id.Zid) (*meta.Meta, error)

func createGetMetadataFunc(ctx context.Context, getMeta usecase.GetMeta) getMetadataFunc {
	return func(zid id.Zid) (*meta.Meta, error) { return getMeta.Run(box.NoEnrichContext(ctx), zid) }
}

type evalMetadataFunc = func(string) ast.InlineSlice

func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
	return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }
}

type getTextTitleFunc func(id.Zid) (string, int)

func (wui *WebUI) makeGetTextTitle(getMetadata getMetadataFunc, evalMetadata evalMetadataFunc) getTextTitleFunc {
func (wui *WebUI) makeGetTextTitle(ctx context.Context, getMeta usecase.GetMeta) getTextTitleFunc {
	return func(zid id.Zid) (string, int) {
		m, err := getMetadata(zid)
		m, err := getMeta.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			if errors.Is(err, &box.ErrNotAllowed{}) {
				return "", -1
			}
			return "", 0
		}
		return encodeEvaluatedTitleText(m, evalMetadata, wui.gentext), 1
		return parser.NormalizedSpacedText(m.GetTitle()), 1
	}
}

func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) string {
func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sxpf.Object {
	is := evalMetadata(value)
	return gen.InlinesString(&is)
	return gen.InlinesSxHTML(&is).Cons(wui.sf.MustMake("span"))
}

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

1
2

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

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

14
15
16
17
18
19
20

-
+











-







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

package webui

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"net/url"
	"strings"

	"zettelstore.de/c/api"
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83

84
85
86
87
88
89
90
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







+









+







		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base)
		wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct {
			Title         string
			SearchURL     string
			QueryValue    string
			QueryKeyQuery string
			Content       string
			CanCreate     bool
			CreateURL     string
			QueryKeySeed  string
			Seed          int
		}{
			Title:         wui.listTitleQuery(q),
			SearchURL:     base.SearchURL,
			QueryValue:    q.String(),
			QueryKeyQuery: base.QueryKeyQuery,
			Content:       htmlContent,
			CanCreate:     wui.canCreate(ctx, user),
			CreateURL:     base.CreateNewURL,
			QueryKeySeed:  base.QueryKeySeed,
			Seed:          seed,
		})
	}
}

192
193
194
195
196
197
198
199
200
201



202
193
194
195
196
197
198
199



200
201
202
203







-
-
-
+
+
+

	return val
}

func (wui *WebUI) listTitleQuery(q *query.Query) string {
	if q == nil {
		return wui.rtConfig.GetSiteName()
	}
	var buf bytes.Buffer
	q.PrintHuman(&buf)
	return buf.String()
	var sb strings.Builder
	q.PrintHuman(&sb)
	return sb.String()
}

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
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
22
23
24
25
26
27
28

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

44
45
46
47
48
49
50
51







-
+














-
+







	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
)

// MakeGetRenameZettelHandler creates a new HTTP handler to display the
// HTML rename view of a zettel.
func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, evaluate *usecase.Evaluate) http.HandlerFunc {
func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		m, err := getMeta.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate))
		getTextTitle := wui.makeGetTextTitle(ctx, getMeta)
		uselessFiles := retrieveUselessFiles(m)

		user := server.GetUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Rename Zettel "+zid.String(), "", user, &base)
		wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct {
			Zid             string

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

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Deleted web/adapter/webui/titleenc.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






























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

package webui

import (
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
)

func encodeEvaluatedTitleHTML(m *meta.Meta, evalMetadata evalMetadataFunc, gen *htmlGenerator) string {
	return encodeZmkMetadata(m.GetTitle(), evalMetadata, gen)
}

func encodeEvaluatedTitleText(m *meta.Meta, evalMetadata evalMetadataFunc, enc *textenc.Encoder) string {
	is := evalMetadata(m.GetTitle())
	result, err := encodeInlinesText(&is, enc)
	if err != nil {
		return err.Error()
	}
	return result

}

Changes to web/adapter/webui/webui.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
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

-
+












-






+
+








-
-







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

// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"net/http"
	"strings"
	"sync"
	"time"

	"codeberg.org/t73fde/sxhtml"
	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/template"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
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
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







-
-



















+
+
+















+












-
-













+
+
+







	rtConfig config.Config
	token    auth.TokenManager
	box      webuiBox
	policy   auth.Policy

	evalZettel *usecase.Evaluate

	gentext *textenc.Encoder

	mxCache       sync.RWMutex
	templateCache map[id.Zid]*template.Template

	mxRoleCSSMap sync.RWMutex
	roleCSSMap   map[string]id.Zid

	tokenLifetime time.Duration
	cssBaseURL    string
	cssUserURL    string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	refreshURL    string
	withAuth      bool
	loginURL      string
	logoutURL     string
	searchURL     string
	createNewURL  string

	sf      sxpf.SymbolFactory
	htmlGen *sxhtml.Generator
}

type webuiBox interface {
	CanCreateZettel(ctx context.Context) bool
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool
	AllowRenameZettel(ctx context.Context, zid id.Zid) bool
	CanDeleteZettel(ctx context.Context, zid id.Zid) bool
}

// New creates a new WebUI struct.
func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI {
	loginoutBase := ab.NewURLBuilder('i')
	sf := sxpf.MakeMappedFactory()
	wui := &WebUI{
		log:      log,
		debug:    kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,

		evalZettel: evalZettel,

		gentext: textenc.Create(),

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration),
		cssBaseURL:    ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(),
		cssUserURL:    ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(),
		homeURL:       ab.NewURLBuilder('/').String(),
		listZettelURL: ab.NewURLBuilder('h').String(),
		listRolesURL:  ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(),
		listTagsURL:   ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(),
		refreshURL:    ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(),
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),

		sf:      sf,
		htmlGen: sxhtml.NewGenerator(sf),
	}
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) observe(ci box.UpdateInfo) {
314
315
316
317
318
319
320
321

322
323
324
325
326
327
328
329
330
331
316
317
318
319
320
321
322

323



324
325
326
327
328
329
330







-
+
-
-
-







	data.QueryKeyQuery = api.QueryKeyQuery
	data.QueryKeySeed = api.QueryKeySeed
	data.CreateNewURL = wui.createNewURL
	data.FooterHTML = wui.calculateFooterHTML(ctx)
	data.DebugMode = wui.debug
}

func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return createGenerator(wui, "") }
func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return wui.createGenerator(wui) }
func (wui *WebUI) createZettelEncoder(ctx context.Context, m *meta.Meta) *htmlGenerator {
	return createGenerator(wui, wui.rtConfig.Get(ctx, m, config.KeyMarkerExternal))
}

// htmlAttrNewWindow returns HTML attribute string for opening a link in a new window.
// If hasURL is false an empty string is returned.
func htmlAttrNewWindow(hasURL bool) string {
	if hasURL {
		return ` target="_blank" rel="noopener noreferrer"`
	}
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
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







-
-
-

-
+










-
+
-
-
-
+
+







		m, err2 := wui.box.GetMeta(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, m) {
			continue
		}
		title := m.GetTitle()
		astTitle := parser.ParseMetadataNoLink(title)
		menuTitle := wui.getSimpleHTMLEncoder().InlinesString(&astTitle)
		result = append(result, simpleLink{
			Text: menuTitle,
			Text: parser.NormalizedSpacedText(m.GetTitle()),
			URL: wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())).
				AppendKVQuery(queryKeyAction, valueActionNew).String(),
		})
	}
	return result
}

func (wui *WebUI) calculateFooterHTML(ctx context.Context) string {
	if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := encoder.Create(api.EncoderHTML)
			htmlEnc := wui.getSimpleHTMLEncoder()
			var buf bytes.Buffer
			if _, err2 = htmlEnc.WriteBlocks(&buf, &zn.Ast); err2 == nil {
				return buf.String()
			if result, err3 := htmlEnc.BlocksString(&zn.Ast); err3 == nil {
				return result
			}
		}
	}
	return ""
}

func (wui *WebUI) renderTemplate(
426
427
428
429
430
431
432
433

434
435
436
437
438
439
440
421
422
423
424
425
426
427

428
429
430
431
432
433
434
435







-
+







		return
	}
	if user := server.GetUser(ctx); user != nil {
		if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil {
			wui.setToken(w, tok)
		}
	}
	var content bytes.Buffer
	var content strings.Builder
	err = t.Render(&content, data)
	if err == nil {
		wui.prepareAndWriteHeader(w, code)
		base.Content = content.String()
		err = bt.Render(w, base)
	}
	if err != nil {

Changes to web/content/content.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------
33
34
35
36
37
38
39

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

42
43
44
45
46
47
48







+

-







	mimeWEBP     = "image/webp"
)

var encoding2mime = map[api.EncodingEnum]string{
	api.EncoderHTML:  mimeHTML,
	api.EncoderMD:    mimeMarkdown,
	api.EncoderSexpr: PlainText,
	api.EncoderSHTML: PlainText,
	api.EncoderText:  PlainText,
	api.EncoderZJSON: JSON,
	api.EncoderZmk:   PlainText,
}

// MIMEFromEncoding returns the MIME encoding for a given zettel encoding
func MIMEFromEncoding(enc api.EncodingEnum) string {
	if m, found := encoding2mime[enc]; found {
		return m

Changes to web/content/content_test.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to web/server/impl/http.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to web/server/impl/impl.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to web/server/impl/router.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to web/server/server.go.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







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

Changes to www/build.md.

1
2
3
4
5
6
7
8

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








+







# How to build Zettelstore
## Prerequisites
You must install the following software:

* A current, supported [release of Go](https://go.dev/doc/devel/release),
* [staticcheck](https://staticcheck.io/),
* [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow),
* [unparam](https://mvdan.cc/unparam),
* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),
* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).

See folder <tt>docs/development</tt> (a zettel box) for details.

## Clone the repository
Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki).

Changes to www/changes.wiki.

1
2
























3
4


















































5
6






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

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


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

-
+
+
+
+
+
+



















-
+







<title>Change Log</title>

<a id="0_12"></a>
<h2>Changes for Version 0.12.0 (pending)</h2>
  *  Query expression is allowed to search for the "context" of a zettel.
     Previously, this was a separate call, without adding a search expression
     / action expression.
     (breaking)
  *  Render footer zettel on all WebUI pages.
     (fix: webui)
  *  New metadata key <tt>expire</tt> records a timestamp when a zettel should
     be treated as, well, expired.
     (minor)
  *  Metadata keys with suffix <tt>-date</tt> and <tt>-time</tt> are treated as
     timestamp values.
     (minor)
  *  <tt>sexpr</tt> zettel encoding is now documented in the manual.
     (minor: manual)
  *  Build tool allows to install / update external Go tools needed to build
     the software.
     (minor)
  *  The use of the <tt>json</tt> zettel encoding is deprecated. Support for
     this encoding may be removed in future versions. Please use the new
     <tt>data</tt> encoding instead.
     (deprecated: api)

<a name="0_11"></a>
<h2>Changes for Version 0.11.0 (pending)</h2>
<a id="0_11"></a>
<h2>Changes for Version 0.11.2 (2023-04-16)</h2>
  *  Render footer zettel on all WebUI pages.
     Backported from 0.12.0.
     (fix: webui)

<h2>Changes for Version 0.11.1 (2023-03-28)</h2>
  *  Make <tt>PICK</tt> search term a little bit more deterministic so that the
     &ldquo;Save As Zettel&rdquo; button produces the same list.
     (fix: webui)

<h2>Changes for Version 0.11.0 (2023-03-27)</h2>
  *  Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
     encoding instead.
     (breaking)
  *  Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
     just a plain string, possibly empty. Therefore, no inline formatting (like
     bold text), no links, no footnotes, no citations (the latter made
     rendering the title often questionable, in some contexts). If you used
     special entities, please use the unicode characters directly. However, as
     a good practice, it is often the best to printable ASCII characters.
     (breaking)
  *  Remove runtime configuration <tt>marker-external</tt>. It was added in
     version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
     change the marker for an external URL, you could modify zettel
     00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
     (Zettelstore User CSS, preferred) by changing / adding a rule to add some
     content after an external <tt><a ...></tt> tag.
     (breaking: webui)
  *  Add SHTML encoding. This allows to ensure the quality of generated HTML
     code. In addition, clients might use it, because it is easier to parse and
     manipulate than ordinary HTML. In the future, HTML template zettel will
     probably also use SHTML, deprecating the current Mustache syntax (which
     was added in [#0_0_9|0.0.9]).
     (major)
  *  Search term <tt>PICK n</tt>, where <tt>n</tt> is an integer value greater
     zero, will pick randomly <tt>n</tt> elements from the search result list.
     Somehow similar (and faster) as <tt>RANDOM LIMIT n</tt>, but allows also
     later ordering of the resulting list.
     (minor)
  *  Changed cost model for zettel context: a zettel with more
     outgoing/incoming references has higher cost than a zettel with less
     references. Also added support for traversing tags, with a similar cost
     model. As an effect, zettel hubs (in many cases your home zettel) will
     less likely add its references. Same for often used tags. The cost model
     might change in some details in the future, but the idea of a penalty
     applied to zettel / tags with many references will hold.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_10"></a>
<a id="0_10"></a>
<h2>Changes for Version 0.10.1 (2023-01-30)</h2>
  *  Show button to save a query into a zettel only when the current user has
     authorization to do it.
     (fix: webui)

<h2>Changes for Version 0.10.0 (2023-01-24)</h2>
  *  Remove support for endpoints <tt>/j, /m, /q, /p, /v</tt>. Their functions
     are merged into endpoint <tt>/z</tt>. This was announced in version 0.9.0.
     Please use only client library with at least version 0.10.0 too.
     (breaking: api)
  *  Remove support for runtime configuration key <tt>footer-html</tt>. Use
     <tt>footer-zettel</tt> instead. Deprecated in version 0.9.0.
     (breaking: webui)
  *  Save a query into a zettel to freeze it.
     (major: webui)
  *  Allow to show all used metadata keys, linked with their occurrences and
     their values.
     (minor: webui)
  *  Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding
     instead.
     (deprecated)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_9"></a>
<a id="0_9"></a>
<h2>Changes for Version 0.9.0 (2022-12-12)</h2>
  *  Remove support syntax <tt>pikchr</tt>. Although it was a nice idea to
     include it into Zettelstore, the implementation is too brittle (w.r.t. the
     expected long lifetime of Zettelstore). There should be other ways to
     support SVG front-ends.
     (breaking)
  *  Allow to upload content when creating / updating a zettel.
69
70
71
72
73
74
75
76

77
78
79
80
81
82
83
146
147
148
149
150
151
152

153
154
155
156
157
158
159
160







-
+







     endpoint are still available and the new funnctionality is still there.
     This is a warning to prepare for some breaking changes in v0.10.0. This
     also affects the API client implementation.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_8"></a>
<a id="0_8"></a>
<h2>Changes for Version 0.8.0 (2022-10-20)</h2>
  *  Remove support for tags within zettel content. Removes also property
     metadata keys <tt>all-tags</tt> and <tt>computed-tags</tt>. Deprecated in
     version 0.7.0.
     (breaking: zettelmarkup, api, webui)
  *  Remove API endpoint <tt>/m</tt>, which retrieve aggregated (tags, roles)
     zettel identifier. Deprecated in version 0.7.0.
109
110
111
112
113
114
115
116

117
118
119
120
121
122
123
186
187
188
189
190
191
192

193
194
195
196
197
198
199
200







-
+







     enabled.
     (minor: server)
  *  Fix error that a manual zettel deletion was not always detected.
     (bug: dirbox)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_7"></a>
<a id="0_7"></a>
<h2>Changes for Version 0.7.1 (2022-09-18)</h2>
  *  Produce a RSS feed compatible to Miniflux.
     (minor)
  *  Make sure to always produce a pubdata in RSS feed.
     (bug)
  *  Prefix search for data that looks like a zettel identifier may end with a
     <tt>0</tt>.
196
197
198
199
200
201
202
203

204
205
206
207
208
209
210
273
274
275
276
277
278
279

280
281
282
283
284
285
286
287







-
+







  *  Cleanup names for HTTP query parameter for API calls. Essentially,
     underscore characters in front are removed. Please use new names, old
     names will be deprecated in version 0.8.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_6"></a>
<a id="0_6"></a>
<h2>Changes for Version 0.6.2 (2022-08-22)</h2>
  *  Recognize renaming of zettel file external to Zettelstore.
     (bug)

<h2>Changes for Version 0.6.1 (2022-08-22)</h2>
  *  Ignore empty tags when reading metadata.
     (bug)
237
238
239
240
241
242
243
244

245
246
247
248
249
250
251
314
315
316
317
318
319
320

321
322
323
324
325
326
327
328







-
+







     in version 0.7. Replace these with the more useable search expressions.
     Please be aware that the = search operator is also deprecated. It was only
     introduced to help the migration.
     (deprecated: api, webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_5_0"></a>
<a id="0_5_0"></a>
<h2>Changes for Version 0.5.1 (2022-08-02)</h2>
  *  Log missing authentication tokens in debug level (was: sense level)
     (major)
  *  Allow to use empty metadata values of string and zmk types.
     (minor)
  *  Add IP address to some log messages, esp. when authentication fails.
     (minor)
302
303
304
305
306
307
308
309

310
311
312
313
314
315
316
379
380
381
382
383
384
385

386
387
388
389
390
391
392
393







-
+







     (minor: web server)
  *  New startup configuration key <kbd>max-request-size</kbd> to limit a web
     request body to prevent client sending too large requests.
     (minor: web server)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_4"></a>
<a id="0_4"></a>
<h2>Changes for Version 0.4 (2022-03-08)</h2>
  *  Encoding &ldquo;djson&rdquo; renamed to &ldquo;zjson&rdquo; (<em>zettel
     json</em>).
     (breaking: api; minor: webui)
  *  Remove inline quotation syntax <tt>&lt;&lt;...&lt;&lt;</tt>. Now,
     <tt>&quot;&quot;...&quot;&quot;</tt> generates the equivalent code.
     Typographical quotes are generated by the browser, not by Zettelstore.
358
359
360
361
362
363
364
365

366
367
368
369
370
371
372
435
436
437
438
439
440
441

442
443
444
445
446
447
448
449







-
+







     (minor: box)
  *  Disallow to cache the authentication cookie. Will remove most unexpected
     log-outs when using a mobile device.
     (minor: webui)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_3"></a>
<a id="0_3"></a>
<h2>Changes for Version 0.3 (2022-02-09)</h2>
  *  Zettel files with extension <tt>.meta</tt> are now treated as content
     files. Previoulsy, they were interpreted as metadata files. The
     interpretation as metadata files was deprecated in version 0.2.
     (breaking: directory and file/zip box)
  *  Add syntax &ldquo;draw&rdquo; to produce some graphical representations.
     (major)
380
381
382
383
384
385
386
387

388
389
390
391
392
393
394
457
458
459
460
461
462
463

464
465
466
467
468
469
470
471







-
+







     (minor: api)
  *  A previously duplicate file that is now useful (because another file was
     deleted) is now logged as such.
     (minor: directory and file/zip box)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_2"></a>
<a id="0_2"></a>
<h2>Changes for Version 0.2 (2022-01-19)</h2>
  *  v0.2.1 (2021-02-01) updates the license year in some documents
  *  Remove support for <tt>;;small text;;</tt> Zettelmarkup.
     (breaking: Zettelmarkup)
  *  On macOS, the downloadable executable program is now called
     &ldquo;zettelstore&rdquo;, as on all other Unix-like platforms.
     (possibly breaking: macOS)
446
447
448
449
450
451
452
453

454
455
456
457
458
459
460
523
524
525
526
527
528
529

530
531
532
533
534
535
536
537







-
+







     of just a boolean value that there were duplicate file names.
     (minor)
  *  Document autostarting Zettelstore on Windows, macOS, and Linux.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_1"></a><a name="0_1_0"></a>
<a id="0_1"></a><a id="0_1_0"></a>
<h2>Changes for Version 0.1 (2021-11-11)</h2>
  *  v0.1.3 (2021-12-15) fixes a bug where the modification date could be set
     when a new zettel is created.
  *  v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when
     more than one comparison is negated.
  *  v0.1.1 (2021-11-12) updates the documentation, mostly related to the
     deprecation of the <tt>//</tt> markup.
511
512
513
514
515
516
517
518

519
520
521
522
523
524
525
588
589
590
591
592
593
594

595
596
597
598
599
600
601
602







-
+







  *  Separate repository for [https://zettelstore.de/contrib/|contributed]
     software. First entry is a software for creating a presentation by using
     zettel.
     (info)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_15"></a>
<a id="0_0_15"></a>
<h2>Changes for Version 0.0.15 (2021-09-17)</h2>
  *  Move again endpoint characters for authentication to make room for future
     features. WebUI authentication moves from <tt>/a</tt> to <tt>/i</tt>
     (login) and <tt>/i?logout</tt> (logout). API authentication moves from
     <tt>/v</tt> to </tt>/a</tt>. JSON-based basic zettel handling moves from
     <tt>/z</tt> to <tt>/j</tt> and <tt>/z/{ID}</tt> to <tt>/j/{ID}</tt>. Since
     the API client is updated too, this should not be a breaking change for
566
567
568
569
570
571
572
573

574
575
576
577
578
579
580
643
644
645
646
647
648
649

650
651
652
653
654
655
656
657







-
+







     zettel in various encodings.
     (minor: api)
  *  Fix: do not list a shadowed zettel that matches the select criteria.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_14"></a>
<a id="0_0_14"></a>
<h2>Changes for Version 0.0.14 (2021-07-23)</h2>
  *  Rename &ldquo;place&rdquo; into &ldquo;box&rdquo;. This also affects the
     configuration keys to specify boxes <tt>box-uri<em>X</em></tt> (previously
     <tt>place-uri-<em>X</em></tt>. Older changes documented here are renamed
     too.
     (breaking)
  *  Add API for creating, updating, renaming, and deleting zettel.
615
616
617
618
619
620
621
622

623
624
625
626
627
628
629
692
693
694
695
696
697
698

699
700
701
702
703
704
705
706







-
+







     (bug)
  *  The phase &ldquo;filtering a zettel list&rdquo; is more precise
     &ldquo;selecting zettel&rdquo;
     (documentation)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_13"></a>
<a id="0_0_13"></a>
<h2>Changes for Version 0.0.13 (2021-06-01)</h2>
  *  Startup configuration <tt>box-<em>X</em>-uri</tt> (where <em>X</em> is a
     number greater than zero) has been renamed to
     <tt>box-uri-<em>X</em></tt>.
     (breaking)
  *  Web server processes startup configuration <tt>url-prefix</tt>. There is
     no need for stripping the prefix by a front-end web server any more.
659
660
661
662
663
664
665
666

667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685

686
687
688
689
690
691
692
736
737
738
739
740
741
742

743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761

762
763
764
765
766
767
768
769







-
+


















-
+







  *  Add zettelmarkup syntax for a table row that should be ignored:
     <tt>|%</tt>. This allows to paste output of the administrator console into
     a zettel.
     (minor: zmk)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_12"></a>
<a id="0_0_12"></a>
<h2>Changes for Version 0.0.12 (2021-04-16)</h2>
  *  Raise the per-process limit of open files on macOS to 1.048.576. This
     allows most macOS users to use at least 500.000 zettel. That should be
     enough for the near future.
     (major)
  *  Mitigate the shortcomings of the macOS version by introducing types of
     directory boxes. The original directory box type is now called "notify"
     (the default value). There is a new type called "simple". This new type
     does not notify Zettelstore when some of the underlying Zettel files
     change.
     (major)
  *  Add new startup configuration <tt>default-dir-box-type</tt>, which gives
     the default value for specifying a directory box type. The default value
     is &ldquo;notify&rdquo;. On macOS, the default value may be changed
     &ldquo;simple&rdquo; if some errors occur while raising the per-process
     limit of open files.
     (minor)

<a name="0_0_11"></a>
<a id="0_0_11"></a>
<h2>Changes for Version 0.0.11 (2021-04-05)</h2>
  *  New box schema "file" allows to read zettel from a ZIP file.
     A zettel collection can now be packaged and distributed easier.
     (major: server)
  *  Non-restricted search is a full-text search. The search string will be
     normalized according to Unicode NFKD. Every character that is not a letter
     or a number will be ignored for the search. It is sufficient if the words
719
720
721
722
723
724
725
726

727
728
729
730
731
732
733
796
797
798
799
800
801
802

803
804
805
806
807
808
809
810







-
+







A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
  #  You update the per-process limit of open files on macOS.
  #  You setup a virtualization environment to run Zettelstore on Linux or Windows.
  #  You wait for version 0.0.12 which addresses this issue.

<a name="0_0_10"></a>
<a id="0_0_10"></a>
<h2>Changes for Version 0.0.10 (2021-02-26)</h2>
  *  Menu item &ldquo;Home&rdquo; now redirects to a home zettel.
     Its default identifier is <tt>000100000000</tt>.
     The identifier can be changed with configuration key <tt>home-zettel</tt>, which supersedes key <tt>start</tt>.
     The default home zettel contains some welcoming information for the new user.
     (major: webui)
  *  Show context of a zettel by following all backward and/or forward reference
763
764
765
766
767
768
769
770

771
772
773
774
775
776
777
840
841
842
843
844
845
846

847
848
849
850
851
852
853
854







-
+







     (&ldquo;&#8599;&#65038;&rdquo;), which also needed the additional
     &ldquo;&amp;#xfe0e;&rdquo; to disable the conversion to an emoji on iPadOS.
     (minor: webui)
  *  A pre-build binary for macOS ARM64 (also known as Apple silicon) is available.
     (minor: infrastructure)
  *  Many smaller bug fixes and improvements, to the software and to the documentation.

<a name="0_0_9"></a>
<a id="0_0_9"></a>
<h2>Changes for Version 0.0.9 (2021-01-29)</h2>
This is the first version that is managed by [https://fossil-scm.org|Fossil]
instead of GitHub. To access older versions, use the Git repository under
[https://github.com/zettelstore/zettelstore-github|zettelstore-github].

<h3>Server / API</h3>
  *  (major) Support for property metadata.
843
844
845
846
847
848
849
850

851
852
853
854
855
856
857
920
921
922
923
924
925
926

927
928
929
930
931
932
933
934







-
+







             <tt>default-title</tt> in info page changed to a full HTML output
             for these Zettelmarkup encoded values.
  *  (minor) Always show the zettel identifier on the zettel detail view.
             Previously, the identifier was not shown if the zettel was not
             editable.
  *  (minor) Do not show computed metadata in edit forms anymore.

<a name="0_0_8"></a>
<a id="0_0_8"></a>
<h2>Changes for Version 0.0.8 (2020-12-23)</h2>
<h3>Server / API</h3>
  *  (bug) Zettel files with extension <tt>.jpg</tt> and without metadata will
           get a <tt>syntax</tt> value &ldquo;jpg&rdquo;. The internal data
           structure got the same value internally, instead of
           &ldquo;jpeg&rdquo;. This has been fixed for all possible alternative
           syntax values.
931
932
933
934
935
936
937
938

939
940
941
942
943
944
945
946

947
948
949
950
951
952
953
1008
1009
1010
1011
1012
1013
1014

1015
1016
1017
1018
1019
1020
1021
1022

1023
1024
1025
1026
1027
1028
1029
1030







-
+







-
+







  *  (minor) If started in simple mode, e.g. via double click or without any
             command, some information for the new user is presented. In the
             terminal, there is a hint about opening the web browser and use
             a specific URL. A <i>Welcome zettel</i> is created, to give some
             more information. (This change also applies to the server itself,
             but it is more suited to the WebUI user.)

<a name="0_0_7"></a>
<a id="0_0_7"></a>
<h2>Changes for Version 0.0.7 (2020-11-24)</h2>
  *  With this version, Zettelstore and this manual got a new license, the
     [https://joinup.ec.europa.eu/collection/eupl|European Union Public
     Licence] (EUPL), version 1.2 or later. Nothing else changed. If you want
     to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to
     fork from the previous version.

<a name="0_0_6"></a>
<a id="0_0_6"></a>
<h2>Changes for Version 0.0.6 (2020-11-23)</h2>
<h3>Server</h3>
  *  (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to
             <tt>00000000000100</tt> (previously <tt>00000000000001</tt>). This
             is done to gain some free identifier with smaller number to be
             used internally. <b>If you customized this zettel, please make
             sure to rename it to the new identifier.</b>
993
994
995
996
997
998
999
1000

1001
1002
1003
1004
1005
1006
1007
1070
1071
1072
1073
1074
1075
1076

1077
1078
1079
1080
1081
1082
1083
1084







-
+







             nor host name, are considered &ldquo;local references&rdquo; (in
             contrast to &ldquo;zettel references&rdquo; and &ldquo;external
             references&rdquo;). When a local reference is displayed as an URL
             on the WebUI, it will not opened in a new window/tab. They will
             receive a <i>local</i> marker, when encoded as &ldquo;djson&rdquo;
             or &ldquo;native&rdquo;. Local references are listed on the
             <i>Info page</i> of each zettel.
  *  (minor) Change the default value for some visual sugar putd after an
  *  (minor) Change the default value for some visual sugar put after an
             external URL to <tt>&\#8599;&\#xfe0e;</tt>
             (&ldquo;&#8599;&#xfe0e;&rdquo;). This affects the former key
             <tt>icon-material</tt> of the <i>Zettelstore Runtime
             Configuration</i>, which is renamed to <tt>marker-external</tt>.
  *  (major) Allow multiple zettel to act as templates for creating new zettel.
             All zettel with a role value &ldquo;new-template&rdquo; act as
             a template to create a new zettel. The WebUI menu item
1027
1028
1029
1030
1031
1032
1033
1034

1035
1036
1037
1038
1039
1040
1041

1042
1043
1044
1045
1046
1047

1048
1049
1050
1051
1052
1053

1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064

1065
1066
1104
1105
1106
1107
1108
1109
1110

1111
1112
1113
1114
1115
1116
1117

1118
1119
1120
1121
1122
1123

1124
1125
1126
1127
1128
1129

1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140

1141
1142
1143







-
+






-
+





-
+





-
+










-
+


             <tt>list-page-size</tt> is greater than zero, the number of WebUI
             list elements will be restricted and it is possible to change to
             the next/previous page to list more elements.
  *  (minor) Change CSS to enhance reading: make <code>line-height</code>
             a little smaller (previous: 1.6, now 1.4) and move list items to
             the left.

<a name="0_0_5"></a>
<a id="0_0_5"></a>
<h2>Changes for Version 0.0.5 (2020-10-22)</h2>
  *  Application Programming Interface (API) to allow external software to
     retrieve zettel data from the Zettelstore.
  *  Specify boxes, where zettel are stored, via an URL.
  *  Add support for a custom footer.

<a name="0_0_4"></a>
<a id="0_0_4"></a>
<h2>Changes for Version 0.0.4 (2020-09-11)</h2>
  *  Optional user authentication/authorization.
  *  New sub-commands <tt>file</tt> (use Zettelstore as a command line filter),
     <tt>password</tt> (for authentication), and <tt>config</tt>.

<a name="0_0_3"></a>
<a id="0_0_3"></a>
<h2>Changes for Version 0.0.3 (2020-08-31)</h2>
  *  Starting Zettelstore has been changed by introducing sub-commands.
     This change is also reflected on the server installation procedures.
  *  Limitations on renaming zettel has been relaxed.

<a name="0_0_2"></a>
<a id="0_0_2"></a>
<h2>Changes for Version 0.0.2 (2020-08-28)</h2>
  *  Configuration zettel now has ID <tt>00000000000001</tt> (previously:
     <tt>00000000000000</tt>).
  *  The zettel with ID <tt>00000000000000</tt> is no longer shown in any
     zettel list. If you changed the configuration zettel, you should rename it
     manually in its file directory.
  *  Creating a new zettel is now done by cloning an existing zettel.
     To mimic the previous behaviour, a zettel with ID <tt>00000000040001</tt>
     is introduced. You can change it if you need a different template zettel.

<a name="0_0_1"></a>
<a id="0_0_1"></a>
<h2>Changes for Version 0.0.1 (2020-08-21)</h2>
  *  Initial public release.

Changes to www/download.wiki.

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

13
14
15


16
17
18



19
20
21
22
23
24

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











-
+

-
-
+
+
-
-
-
+
+
+





-
+


<title>Download</title>
<h1>Download of Zettelstore Software</h1>
<h2>Foreword</h2>
  *  Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later.
  *  The software is provided as-is.
  *  There is no guarantee that it will not damage your system.
  *  However, it is in use by the main developer since March 2020 without any damage.
  *  It may be useful for you. It is useful for me.
  *  Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

<h2>ZIP-ped Executables</h2>
Build: <code>v0.10.0</code> (2023-01-24).
Build: <code>v0.11.1</code> (2023-03-28).

  *  [/uv/zettelstore-0.10.0-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.10.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.11.1-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.11.1-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.10.0-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.10.0-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.10.0-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/uv/zettelstore-0.11.1-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.11.1-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/uv/zettelstore-0.11.1-windows-amd64.zip|Windows] (amd64)

Unzip the appropriate file, install and execute Zettelstore according to the manual.

<h2>Zettel for the manual</h2>
As a starter, you can download the zettel for the manual
[/uv/manual-0.10.0.zip|here].
[/uv/manual-0.11.1.zip|here].
Just unzip the contained files and put them into your zettel folder or
configure a file box to read the zettel directly from the ZIP file.

Changes to www/index.wiki.

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







-
+

-
-
-
-
-
+
+
+
+
+





-
+





[https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
software, which often connects to Zettelstore via its API. Some of the software
packages may be experimental.

[https://twitter.com/zettelstore|Stay]
<a rel="me" href="https://nerdculture.de/@zettelstore">tuned</a>&nbsp;&hellip;
<hr>
<h3>Latest Release: 0.10.0 (2022-12-12)</h3>
<h3>Latest Release: 0.11.1 (2023-03-28)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_10|Change summary]
  *  [/timeline?p=v0.10.0&bt=v0.9.0&y=ci|Check-ins for version 0.10.0],
     [/vdiff?to=v0.10.0&from=v0.9.0|content diff]
  *  [/timeline?df=v0.10.0&y=ci|Check-ins derived from the 0.10.0 release],
     [/vdiff?from=v0.10.0&to=trunk|content diff]
  *  [./changes.wiki#0_11|Change summary]
  *  [/timeline?p=v0.11.1&bt=v0.10.0&y=ci|Check-ins for version 0.11.1],
     [/vdiff?to=v0.11.1&from=v0.10.0|content diff]
  *  [/timeline?df=v0.11.0&y=ci|Check-ins derived from the 0.11.0 release],
     [/vdiff?from=v0.11.0&to=trunk|content diff]
  *  [./plan.wiki|Limitations and planned improvements]
  *  [/timeline?t=release|Timeline of all past releases]

<hr>
<h2>Build instructions</h2>
Just install [https://golang.org/dl/|Go] and some Go-based tools. Please read
Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read
the [./build.md|instructions] for details.

  *  [/dir?ci=trunk|Source code]
  *  [/download|Download the source code] as a tarball or a ZIP file
     (you must [/login|login] as user &quot;anonymous&quot;).