Zettelstore

Check-in Differences
Login

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

Difference From v0.11.2 To v0.10.0

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-present Detlef Stern
Copyright (c) 2020-2023 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-present Detlef Stern
## 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.

Changes to VERSION.

1


1
-
+
0.11.2
0.10.0

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

-
+
















+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 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"
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
90
91
92
93
94
95
96















97
98



99

100
101
102
103
104
105
106
107







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


-
-
-

-
+







	// 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 a box, which is not in state StartStateStopped is not allowed.
	// Starting an already started box 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.
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

210
211
212
213
214
215
216
179
180
181
182
183
184
185

186
187
188
189
190
191

192
193
194
195
196
197
198
199







-






-
+








// 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    BaseBox
	Box    Box
	Reason UpdateReason
	Zid    id.Zid
}

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

310
311
312
313
314
315
316

























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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
// 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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/constbox/base.css.

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

147
148
149
150
151
152
153







-







  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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to box/constbox/context.mustache.

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

1
2
3
4
5
6
7
8
9
10
11

-











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

36
37
38
39
40
41
42
43







+
<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-present Detlef Stern
Copyright (c) 2020-2023 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
13

14

1
2
3
4
5
6
7

8
9
10
11

12

-







-
+



-
+
-
<article>
<header>
<h1>{{Title}}</h1>
</header>
<form action="{{{SearchURL}}}">
<input class="zs-input" type="text" placeholder="Search.." name="{{QueryKeyQuery}}" value="{{QueryValue}}">
</form>
{{{Content}}}
{{#CanCreate}}<form action="{{{CreateURL}}}">
<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>{{/CanCreate}}
</form>
</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>{{Heading}}</h1>
<h1>{{{HTMLTitle}}}</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: {{{ExtURL}}}{{/HasExtURL}}
{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
125
126
127
128
129
130
131


















132
133
134
135
136
137
138







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







	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)
		go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc)
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
149
150
151
152
153
154
155

156
157
158
159
160
161
162







-







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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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


















45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70







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







-
+











-







func (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, zb.log, zipNotifier, zb.notify)
	zb.dirSrv = notify.NewDirService(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
21
22
1

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


14
15
16
17
18
19
20

-
+











-
-







//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
// Copyright (c) 2021-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 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) {
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
30
31
32
33
34
35
36
































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

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

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-present Detlef Stern
// Copyright (c) 2021-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 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 sb strings.Builder
	var buf bytes.Buffer
	for i := 0; i < len(mgr.boxes)-2; i++ {
		if i > 0 {
			sb.WriteString(", ")
			buf.WriteString(", ")
		}
		sb.WriteString(mgr.boxes[i].Location())
		buf.WriteString(mgr.boxes[i].Location())
	}
	return sb.String()
	return buf.String()
}

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







-
+






-
+





+
+
-
+














+
+
-
+


-
-











+
+
-
+


-
-














+
+
-
+


-
-











+
+
-
+


-
-











			}
		}
	}
	result := make([]*meta.Meta, 0, len(selected))
	for _, m := range selected {
		result = append(result, m)
	}
	return q.AfterSearch(result), nil
	return q.Sort(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.State() == box.StartStateStarted && mgr.boxes[0].CanUpdateZettel(ctx, zettel)
	return mgr.started && 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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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

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

89
90
91

92
93
94
95
96
97
98
81
82
83
84
85
86
87

88
89
90
91
92
93
94
95
96
97
98
99







-
+



+







		}
	}()

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := box.NoEnrichContext(context.Background())
	for {
		mgr.idxWorkService(ctx)
		// Sleep first, so the indexer will wait for boxes to initialize.
		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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------
81
82
83
84
85
86
87
88
89
90

91
92
93
94
95
96
97
81
82
83
84
85
86
87


88
89
90
91
92
93
94
95
96







-
-

+








// 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
	stateMx      sync.RWMutex
	state        box.StartState
	mgrMx        sync.RWMutex
	started      bool
	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
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
104
105
106
107
108
109
110













111
112
113
114
115
116
117







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







	// 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() {
			propertyKeys.Set(kd.Name)
205
206
207
208
209
210
211
212
213
214
215
216
217
218

219
220
221
222
223
224
225
226
191
192
193
194
195
196
197

198
199
200
201


202

203
204
205
206
207
208
209







-




-
-
+
-








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

241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
224
225
226
227
228
229
230


231
232
233
234
235

236
237
238
239
240
241
242







-
-





-







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







+
-
+
-


-









-





-
+





-
-
-
-
-
-
-
-

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






-
+


-






-
+





+
+
-
+


-
-







	}
}

// 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 {
	defer mgr.mgrMx.Unlock()
		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.setState(box.StartStateStopped)
		mgr.mgrMx.Unlock()
		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
func (mgr *Manager) allBoxesStarted() bool {
	for _, bx := range mgr.boxes {
	mgr.mgrMx.Unlock()
		if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted {
			return false
	return nil
		}
	}
	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.State() != box.StartStateStarted {
	if !mgr.started {
		return
	}
	mgr.setState(box.StartStateStopping)
	close(mgr.done)
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			ss.Stop(ctx)
		}
	}
	mgr.setState(box.StartStateStopped)
	mgr.started = false
}

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

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

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

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

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

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

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
50
51
52
53
54
55
56

57
58
59
60
61
62
63
64









65
66
67
68
69
70
71







-
+







-
-
-
-
-
-
-
-
-







	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{Box: mb, Reason: box.OnZettel, Zid: zid}
		chci <- box.UpdateInfo{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")
	return nil

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
25
26
27
28
29
30
31

32
33
34
35
36
37
38
39
40

41
42
43





44
45
46
47
48
49
50
51
52

53
54
55
56
57

58
59
60
61
62
63
64
65

66
67

68
69
70

71
72








73
74
75
76
77

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

92
93
94
95
96
97
98
99







-
+








-
+


-
-
-
-
-
+
+
+
+
+




-





-
+







-
+

-



-
+

-
-
-
-
-
-
-
-





-
+













-
+







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

type entrySet map[id.Zid]*DirEntry

// DirServiceState signal the internal state of the service.
// directoryState 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 DirServiceState uint8
type directoryState uint8

const (
	DsCreated  DirServiceState = iota
	DsStarting                 // Reading inital scan
	DsWorking                  // Initial scan complete, fully operational
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
	dsCreated  directoryState = 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    DirServiceState
	state    directoryState
	entries  entrySet
}

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

// NewDirService creates a new directory service.
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
func NewDirService(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")
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
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







-
+






-
+








-
+


-
+







	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
		}
324
325
326
327
328
329
330
331

332
333
334
335
336
337
338
314
315
316
317
318
319
320

321
322
323
324
325
326
327
328







-
+







	}
}

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})`)
601
602
603
604
605
606
607
608

609
610
591
592
593
594
595
596
597

598
599
600







-
+


	}
	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{Box: ds.box, Reason: box.OnZettel, Zid: zid}
		chci <- box.UpdateInfo{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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
90
91
92
93
94
95
96

97
98
99
100
101
102
103







-







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

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

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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
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







-
+
+






-
+
+



















-
+
+







		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(ucGetMeta))
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(
			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(ucGetMeta, ucGetAllMeta))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(
			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(ucGetMeta, ucUnlinkedRefs))
	webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(
		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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
18
19
20
21
22
23
24

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







+







)

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

6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22





-
+







-









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

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-present by Detlef Stern <ds@zettelstore.de>
default-copyright: (c) 2020-2022 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: 20230317183435
modified: 20221219175720

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66







+
+
+
+
+
+








  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

1
2
3
4
5
6

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






-
+











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

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: 20230316182711
modified: 20230109105434

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: 20230320150950
modified: 20221004134841

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
121
122
123
124
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







-
-
-














-
-
-










+
+







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


6
7
8
9
10
11
12
13





-
-
+







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

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

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







-
-
-
-
-
-
-




















-
+

-
+







  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 or if the specification contains a valid ''PICK''.
  A random order specification will be ignored, if there is an explicit ordering given.

  Example: ''RANDOM ORDER published'' will be interpreted as ''ORDER published'', ''PICK 3 RANDOM'' will be interpreted as ''PICK 3''.
  Example: ''RANDOM ORDER published'' will be interpreted as ''ORDER published''.
* 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''.
80
81
82
83
84
85
86
87

88
89
90
91
92
72
73
74
75
76
77
78

79












-
+
-
-
-
-
-
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 ''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.
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"".
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
24
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23






-
+









-







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

```
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: 20230327122547
modified: 20220917174956

|= 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:PICK 40]] | 40 random zettel
| [[query:RANDOM LIMIT 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: 20230320151117
modified: 20230116183844

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 have a default value, this precondition should always be true.
For a metadata key like [[''title''|00001006020000#title]], which has 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-present by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-copyright" content="(c) 2020-2022 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: 20230316180859
modified: 20230109104839

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

Added 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: 20230316182622
modified: 20230109104812

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].
It is (relatively) easy to parse and contain all relevant information of a zettel, metadata and content.
Both encodings are (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.

Deleted 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).

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

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

Added 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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------
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-present Detlef Stern
// Copyright (c) 2021-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 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 sb strings.Builder
	var buf bytes.Buffer
	for i, zid := range zs {
		if i > 0 {
			sb.WriteByte(' ')
			buf.WriteByte(' ')
		}
		sb.WriteString(zid.String())
		buf.WriteString(zid.String())
	}
	return sb.String()
	return buf.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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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

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
1

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

-
+












+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 meta provides the domain specific type 'meta'.
package meta

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

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

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

132
133
134
135
136
137
138
139







-
+







	}
	return result
}

// Supported keys.
func init() {
	registerKey(api.KeyID, TypeID, usageComputed, "")
	registerKey(api.KeyTitle, TypeEmpty, usageUser, "")
	registerKey(api.KeyTitle, TypeZettelmarkup, 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, "")
400
401
402
403
404
405
406
407

408
409
410
411
412
413
414


415
416
417
418
419
420
421
422


423
401
402
403
404
405
406
407

408
409
410
411
412
413


414
415
416
417
418
419
420
421


422
423
424







-
+





-
-
+
+






-
-
+
+

// 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 sb strings.Builder
	var buf bytes.Buffer
	for pos < len(s) {
		nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) })
		if nextPos < 0 {
			break
		}
		sb.WriteString(s[pos:nextPos])
		sb.WriteByte(' ')
		buf.WriteString(s[pos:nextPos])
		buf.WriteByte(' ')
		_, size := utf8.DecodeRuneInString(s[nextPos:])
		pos = nextPos + size
	}
	if pos == 0 {
		return strings.TrimSpace(s)
	}
	sb.WriteString(s[pos:])
	return strings.TrimSpace(sb.String())
	buf.WriteString(s[pos:])
	return strings.TrimSpace(buf.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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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
1

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

-
+











+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 meta_test

import (
	"bytes"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)
32
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
49







-
-
-
+
+
+







	if syntax != "" {
		m.Set(api.KeySyntax, syntax)
	}
	return m
}
func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
	t.Helper()
	var sb strings.Builder
	m.Write(&sb)
	if got := sb.String(); got != expected {
	var buf bytes.Buffer
	m.Write(&buf)
	if got := buf.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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------
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: `(BLOCK (BLOB (INLINE (TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`,
			encoderSexpr: `((BLOB ((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
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
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

-
+















+


-
+
-








+


-
+
-








+


-
+
-








+
-
+

-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


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








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-
















+


-
+
-












+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








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

-
+
-








+


-
+
-











+


-
+
-







-
-
+
+

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

-
+
-
-
+







+


-
+
-








+
-
-
+
+
-
-
-
+
+






+

-
+
-









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

var tcsBlock = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderMD:    "",
			encoderSexpr: `(BLOCK)`,
			encoderSexpr: `()`,
			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: `(BLOCK (PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`,
			encoderSexpr: `((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: `(BLOCK (VERBATIM-COMMENT () "No\nrender"))`,
			encoderSexpr: `((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-->\n",
			encoderHTML:  "<!--\nRender\n-->",
			encoderMD:    "",
			encoderSexpr: `(BLOCK (VERBATIM-COMMENT (quote (("-" . ""))) "Render"))`,
			encoderSexpr: `((VERBATIM-COMMENT (("-" "")) "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: `(BLOCK (HEADING 1 () "top" "top" (INLINE (TEXT "Top"))))`,
			encoderSexpr: `((HEADING 1 () "top" "top" (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: `(BLOCK (UNORDERED (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`,
			encoderSexpr: `((UNORDERED ((TEXT "A")) ((TEXT "B")) ((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: `(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")))))`,
			encoderSexpr: `((UNORDERED ((PARA (TEXT "T1")) (UNORDERED ((TEXT "T2")))) ((PARA (TEXT "T3")) (UNORDERED ((TEXT "T4")) ((TEXT "T5")))) ((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: `(BLOCK (UNORDERED (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`,
			encoderSexpr: `((UNORDERED ((TEXT "Item1.1")) ((TEXT "Item1.2")) ((TEXT "Item1.3")) ((TEXT "Item2.1")) ((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: `(BLOCK (THEMATIC ()))`,
			encoderSexpr: `((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: `(BLOCK (PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`,
			encoderSexpr: `((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: `(BLOCK (PARA (TEXT "Text")) (ORDERED (INLINE (TEXT "abc"))))`,
			encoderSexpr: `((PARA (TEXT "Text")) (ORDERED ((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: `(BLOCK (QUOTATION (INLINE (TEXT "ToBeOrNotToBe"))))`,
			encoderSexpr: `((QUOTATION ((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: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOrNotToBe"))) (INLINE (TEXT "Romeo"))))`,
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) ((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: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (INLINE (TEXT "Romeo"))))`,
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) ((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: "(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\"))))",
			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\"))))",
			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: `(BLOCK (REGION-BLOCK () (BLOCK (PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) (INLINE)))`,
			encoderSexpr: `((REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) ()))`,
			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: `(BLOCK (VERBATIM-CODE () "Hello\nWorld"))`,
			encoderSexpr: `((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: `(BLOCK (VERBATIM-CODE (quote (("-" . ""))) "Hello World"))`,
			encoderSexpr: `((VERBATIM-CODE (("-" "")) "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: `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`,
			encoderSexpr: `((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: `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`,
			encoderSexpr: `((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{
			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>",
			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"}]}]]}]}]`,
			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>",
			encoderHTML:  "<dl><dt>Zettel</dt><dd>Paper</dd><dd>Note</dd><dt>Zettelkasten</dt><dd>Slip box</dd></dl>",
			encoderMD:    "",
			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"))))))`,
			encoderSexpr: `((DESCRIPTION ((TEXT "Zettel")) (((TEXT "Paper")) ((TEXT "Note"))) ((TEXT "Zettelkasten")) (((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: `(BLOCK (TABLE () (LIST (CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) (LIST (CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,
			encoderSexpr: `((TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((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: `(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")))))`,
			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")))))`,
			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 Endnote",
		zmk:   `Text[^Endnote]`,
		descr: "Simple Endnotes",
		zmk:   `Text[^Footnote]`,
		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",
			encoderZmk:   useZmk,
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Footnote","i":[{"":"Text","s":"Footnote"}]}]}]`,
		},
	},
	{
		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>",
			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>`,
			encoderMD:    "Text",
			encoderSexpr: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (quote (INLINE (TEXT "Endnote") (ENDNOTE () (quote (INLINE (TEXT "Nested")))))))))`,
			encoderSexpr: `((PARA (TEXT "Text") (FOOTNOTE () (TEXT "Footnote"))))`,
			encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderText:  "Text Endnote Nested",
			encoderText:  "Text Footnote",
			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: `(BLOCK (TRANSCLUDE (quote (("width" . "100px"))) (quote (EXTERNAL "http://example.com/image"))))`,
			encoderSexpr: `((TRANSCLUDE (("width" "100px")) (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: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`,
			encoderHTML:  ``,
			encoderSexpr: `((PARA (LITERAL-COMMENT () "Comment")))`,
			// encoderSHTML: ``,
			encoderText: "",
			encoderZmk:  useZmk,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderSexpr: `(BLOCK)`,
			encoderSexpr: `()`,
			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
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
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

-
+















+


-
+
-








+


-
+
-





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



+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-





-
-
-
-
-
-
-
-
-
-
-



+


-
+
-








+


-
+
-








+


-
-
+








+


-
+
-








+
-
+

-
+
-





-
-
+
+

+


-
+
-
-
+







+


-
+
-








+


-
+
-

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







+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-








+


-
+
-






-
+

+


-
+

-







+


-
+
-








+


-
+
-








+


-
+
-








+
-
+

-
+
-








+


-
+
-








+


-
+
-








+


-
+
-





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

var tcsInline = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing (inline)",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderMD:    "",
			encoderSexpr: `(INLINE)`,
			encoderSexpr: `()`,
			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: `(INLINE (TEXT "Hello,") (SPACE) (TEXT "world"))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-EMPH () (TEXT "emph")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-STRONG () (TEXT "strong")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-INSERT () (TEXT "insert")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-DELETE () (TEXT "delete")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-SUPER () (TEXT "superscript")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-SUB () (TEXT "subscript")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-QUOTE (quote (("lang" . "de"))) (TEXT "quotes")))`,
			encoderSexpr: `((FORMAT-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: `(INLINE (FORMAT-SPAN () (TEXT "span")))`,
			encoderSexpr: `((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: `(INLINE (LITERAL-CODE () "code"))`,
			encoderSexpr: `((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: `(INLINE (LITERAL-CODE (quote (("-" . ""))) "x y"))`,
			encoderSexpr: `((LITERAL-CODE (("-" "")) "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: `(INLINE (LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`,
			encoderSexpr: `((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: `(INLINE (LITERAL-INPUT () "input"))`,
			encoderSexpr: `((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: `(INLINE (LITERAL-OUTPUT () "output"))`,
			encoderSexpr: `((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: `(INLINE (LITERAL-MATH () "\\TeX"))`,
			encoderSexpr: `((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: `(INLINE (FORMAT-SPAN (quote (("lang" . "fr"))) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSexpr: `((FORMAT-SPAN (("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: `(INLINE (CITE () "Stern18"))`,
			encoderSexpr: `((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: `(INLINE (TEXT "%") (SPACE) (TEXT "comment"))`,
			encoderSexpr: `((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: `(INLINE (LITERAL-COMMENT () "line comment"))`,
			encoderSexpr: `((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"))`,
			encoderSHTML: `((@@ "line comment"))`,
			encoderSexpr: `((LITERAL-COMMENT (("-" "")) "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: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment"))`,
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "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 -&#45;> end -->`,
			encoderHTML:  `Text<!-- comment --&gt; end -->`,
			encoderMD:    "Text",
			encoderSexpr: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment --> end"))`,
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment --> end"))`,
			encoderSHTML: `("Text" (@@ "comment --> end"))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple inline endnote",
		zmk:   `[^endnote]`,
		descr: "Simple footnote",
		zmk:   `[^footnote]`,
		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: `(INLINE (ENDNOTE () (quote (INLINE (TEXT "endnote")))))`,
			encoderSexpr: `((FOOTNOTE () (TEXT "footnote")))`,
			encoderSHTML: `((sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1")))`,
			encoderText:  `endnote`,
			encoderText:  `footnote`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple mark",
		zmk:   `[!mark]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Mark","s":"mark","q":"mark"}]`,
			encoderHTML:  `<a id="mark"></a>`,
			encoderMD:    "",
			encoderSexpr: `(INLINE (MARK "mark" "mark" "mark"))`,
			encoderSexpr: `((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: `(INLINE (MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`,
			encoderSexpr: `((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: `(INLINE (LINK-EXTERNAL () "abc"))`,
			encoderSexpr: `((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: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSexpr: `((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: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSexpr: `((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: `(INLINE (LINK-ZETTEL () "00000000000100"))`,
			encoderSexpr: `((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: `(INLINE (LINK-ZETTEL () "00000000000100" (TEXT "Config")))`,
			encoderSexpr: `((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: `(INLINE (LINK-ZETTEL () "00000000000100#frag"))`,
			encoderSexpr: `((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: `(INLINE (LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`,
			encoderSexpr: `((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: `(INLINE (LINK-SELF () "#frag"))`,
			encoderSexpr: `((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: `(INLINE (LINK-HOSTED () "/hosted" (TEXT "H")))`,
			encoderSexpr: `((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: `(INLINE (LINK-BASED () "/based" (TEXT "B")))`,
			encoderSexpr: `((LINK-HOSTED () "/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: `(INLINE (LINK-HOSTED () "../relative" (TEXT "R")))`,
			encoderSexpr: `((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: `(INLINE (LINK-QUERY () "title:syntax"))`,
			encoderSexpr: `((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: `(INLINE (LINK-QUERY () "title:syntax" (TEXT "Q")))`,
			encoderSexpr: `((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 src="abc">`,
			encoderHTML:  `<img alt="alternate description missing" src="abc">`,
			encoderMD:    "![abc](abc)",
			encoderSexpr: `(INLINE (EMBED () (quote (EXTERNAL "abc")) ""))`,
			encoderSexpr: `((EMBED () (EXTERNAL "abc") ""))`,
			encoderSHTML: `((img (@ (src . "abc"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Inline HTML Zettel",
		zmk:   `@@<hr>@@{="html"}`,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `(INLINE)`,
			encoderSexpr: `()`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   ``,
		},
	},
	{
		descr: "Inline Text Zettel",
		zmk:   `@@<hr>@@{="text"}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Zettel","a":{"":"text"},"s":"<hr>"}]`,
			encoderHTML:  ``,
			encoderMD:    "<hr>",
			encoderSexpr: `(INLINE (LITERAL-ZETTEL (quote (("" . "text"))) "<hr>"))`,
			encoderSexpr: `((LITERAL-ZETTEL (("" "text")) "<hr>"))`,
			encoderSHTML: `(())`,
			encoderText:  `<hr>`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `(INLINE)`,
			encoderSexpr: `()`,
			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
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

-
+











+

-


-
+

+










-

+
















+



-







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

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

	"codeberg.org/t73fde/sxpf/reader"
	"codeberg.org/t73fde/sxpf"
	"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
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







-
-
-
-
-
-
+







}

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.Errorf("%s\nEncoder:  %s\nError:    %v", prefix, enc, err)
			t.Error("pe.encode:", err)
			continue
		}
		if enc == api.EncoderZmk && exp == useZmk {
			exp = zmkDefault
		}
		if got != exp {
			prefix := fmt.Sprintf("Test #%d", testNum)
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
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







-
+




-
+
+
+
+
+




















-
-
+
+


-
+









-
-
+
+


-
+



	t.Helper()
	encdr := encoder.Create(encoderSexpr)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := reader.MakeReader(strings.NewReader(exp)).Read()
	val, err := sxpf.ParseString(sexpr.Smk, exp)
	if err != nil {
		t.Error(err)
		return
	}
	got := val.Repr()
	got, err := sxpf.Repr(val)
	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 sb strings.Builder
	if _, err := encdr.WriteInlines(&sb, &in.is); err != nil {
	var buf bytes.Buffer
	if _, err := encdr.WriteInlines(&buf, &in.is); err != nil {
		return "", err
	}
	return sb.String(), nil
	return buf.String(), nil
}

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

type peBlocks struct {
	bs ast.BlockSlice
}

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

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

18
19

20
21
22
23
24

25

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



45








46
47
48

49







50







51



52






53
54
55
56
57
58


59
60



61
62
63





64


65







66
67
68
69
70



71


72
73
74
75
76


77





78



79
80
81
82
83
84
85
86



87


88
89


90




91



92
93
94
95
96
97










98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

-
+















-


-
+




-

-



















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


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

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

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




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








-
-
-
+
-
-
+

-
-
+
-
-
-
-
+
-
-
-






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

//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
// Copyright (c) 2022-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 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/shtml"
	"zettelstore.de/c/html"
	"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>")
		isTitle = parser.ParseSpacedText(plainTitle)
		is := evalMeta(plainTitle)
		xtitle := tx.GetSexpr(&isTitle)
		htitle, err = th.Transform(xtitle)
		if err != nil {
			return 0, err
		}
	}

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

		io.WriteString(w, "</title>\n")
	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 {
		var sb strings.Builder
		he.textEnc.WriteInlines(&sb, &isTitle)
			io.WriteString(w, "<h1>")
			if err := acceptInlines(env, &isTitle); err != nil {
		_ = curr.AppendBang(sxpf.Nil().Cons(sxpf.MakeString(sb.String())).Cons(sf.MustMake("title")))
	}

				return 0, err
			}
			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())
	}
	if hen != nil {
		curr = curr.AppendBang(sxpf.Nil().Cons(sf.MustMake("hr")))
		_ = curr.AppendBang(hen)
	}

	doc := sxpf.MakeList(
		sf.MustMake(sxhtml.NameSymDoctype),

	err := acceptBlocks(env, &zn.Ast)
	if err == nil {
		// env.WriteEndnotes()
		io.WriteString(w, "</body>\n</html>")
		sxpf.MakeList(sf.MustMake("html"), head, body),
	)

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

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

	th := shtml.NewTransformer(1, nil)
	hm, err := th.Transform(xm)
	if err != nil {
		return 0, err
	return 0, nil
	}
	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)
	th := shtml.NewTransformer(1, nil)
	env := html.NewEncEnvironment(w, 1)
	hobj, err := th.Transform(xval)

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

		err = env.GetError()
		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) {
	hobj, err := shtmlenc.TransformSlice(is)
	if err == nil {
		gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(hobj))
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}
		return length, nil
	}
	return 0, err
	env := html.NewEncEnvironment(w, 1)
	return 0, acceptInlines(env, is)
}

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)
		io.WriteString(w, `" content="`)
		is := evalMeta(p.Value)
		var sb strings.Builder
		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()
}
func acceptInlines(env *html.EncEnvironment, is *ast.InlineSlice) error {
	lst := sexprenc.GetSexpr(is)
	sxpf.Eval(env, lst)
	return env.GetError()
}

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

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







-
-
-
-
+
+
+


-
+

-
-
+








-
-
+




-
-
+


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 := t.GetSexpr(&zn.Ast)
	meta := t.GetMeta(zn.InhMeta, evalMeta)
	return io.WriteString(w, sxpf.Nil().Cons(content).Cons(meta).Repr())
	content := GetSexpr(&zn.Ast)
	meta := GetMeta(zn.InhMeta, evalMeta)
	return io.WriteString(w, sxpf.NewPair(meta, sxpf.NewPair(content, nil)).String())
}

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

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, t.GetSexpr(bs).Repr())
	return io.WriteString(w, GetSexpr(bs).String())
}

// 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, t.GetSexpr(is).Repr())
	return io.WriteString(w, GetSexpr(is).String())
}

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

-
+











+



-









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


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


-
+






-
+

-
-
-
-
+
+
+
+




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


-
+







-
+



-
+


-
+

-
+


-
+
+
+

-



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



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

-
-
+
+
+
+
+
+
+

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

-
-
+
+
+
+
-
+
+
+

-
-
-
-
+
+
+
+



-
+


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




-
+

-
-
-
+
+
+

-
+



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



-
-
+
+


-
+

-
+

-
-
+
-
-
-
+
-
-
+















-
-
-
+
+
+

-
-
+
+

+
+
+
+
-
+

-
+

-
+

-
+

-
+


-
-
-
-
+
+
+
+

-
+

-
+

-
-
-
-
-
-
-
-
+
+

-
+

+
+
-
+
+
+
+
+
+


-
-
+
+


-
-
+
+

-
+

-
+

-
-
+
+

-
-
+
+



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




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

-
+



-
-
-
+
+
+


-
+

-
+

-
+


+
-
+

-
+

-
+

-
+


-
+




-
+


-
-
-
+
+
+





-
+

-
+

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

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

type Transformer struct {
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
	inVerse bool
}

func (t *Transformer) GetSexpr(node ast.Node) *sxpf.List {
func (t *transformer) getSexpr(node ast.Node) *sxpf.Pair {
	switch n := node.(type) {
	case *ast.BlockSlice:
		return t.getBlockSlice(n)
	case *ast.InlineSlice:
		return t.getInlineSlice(*n)
	case *ast.ParaNode:
		return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara)
		return sxpf.NewPair(sexpr.SymPara, t.getInlineSlice(n.Inlines))
	case *ast.VerbatimNode:
		return sxpf.MakeList(
			mapGetS(t, t.mapVerbatimKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sxpf.MakeString(string(n.Content)),
		return sxpf.NewPairFromValues(
			mapGetS(mapVerbatimKindS, n.Kind),
			getAttributes(n.Attrs),
			sxpf.NewString(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return sxpf.MakeList(
			t.zetSyms.SymHeading,
			sxpf.MakeInteger64(int64(n.Level)),
			t.getAttributes(n.Attrs),
			sxpf.MakeString(n.Slug),
			sxpf.MakeString(n.Fragment),
			t.getInlineSlice(n.Inlines),
		return sxpf.NewPair(
			sexpr.SymHeading,
			sxpf.NewPair(
				sxpf.NewInteger(int64(n.Level)),
				sxpf.NewPair(
					getAttributes(n.Attrs),
					sxpf.NewPair(
						sxpf.NewString(n.Slug),
						sxpf.NewPair(
							sxpf.NewString(n.Fragment),
							t.getInlineSlice(n.Inlines),
						),
					),
				),
			),
		)
	case *ast.HRuleNode:
		return sxpf.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs))
		return sxpf.NewPairFromValues(sexpr.SymThematic, 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.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref))
		return sxpf.NewPairFromValues(sexpr.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sxpf.MakeList(t.zetSyms.SymText, sxpf.MakeString(n.Text))
		return sxpf.NewPairFromValues(sexpr.SymText, sxpf.NewString(n.Text))
	case *ast.SpaceNode:
		if t.inVerse {
			return sxpf.MakeList(t.zetSyms.SymSpace, sxpf.MakeString(n.Lexeme))
			return sxpf.NewPairFromValues(sexpr.SymSpace, sxpf.NewString(n.Lexeme))
		}
		return sxpf.MakeList(t.zetSyms.SymSpace)
		return sxpf.NewPairFromValues(sexpr.SymSpace)
	case *ast.BreakNode:
		if n.Hard {
			return sxpf.MakeList(t.zetSyms.SymHard)
			return sxpf.NewPairFromValues(sexpr.SymHard)
		} else {
			return sxpf.NewPairFromValues(sexpr.SymSoft)
		}
		return sxpf.MakeList(t.zetSyms.SymSoft)
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sxpf.MakeString(n.Syntax)).
			Cons(t.getReference(n.Ref)).
			Cons(t.getAttributes(n.Attrs)).
		return sxpf.NewPair(
			sexpr.SymEmbed,
			sxpf.NewPair(
				getAttributes(n.Attrs),
				sxpf.NewPair(
					getReference(n.Ref),
					sxpf.NewPair(
						sxpf.NewString(n.Syntax),
						t.getInlineSlice(n.Inlines),
			Cons(t.zetSyms.SymEmbed)
					),
				),
			),
		)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sxpf.MakeString(n.Key)).
			Cons(t.getAttributes(n.Attrs)).
		return sxpf.NewPair(
			sexpr.SymCite,
			sxpf.NewPair(
				getAttributes(n.Attrs),
			Cons(t.zetSyms.SymCite)
				sxpf.NewPair(
					sxpf.NewString(n.Key),
					t.getInlineSlice(n.Inlines),
				),
			),
		)
	case *ast.FootnoteNode:
		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)
		return sxpf.NewPair(
			sexpr.SymFootnote,
			sxpf.NewPair(
				getAttributes(n.Attrs),
				t.getInlineSlice(n.Inlines),
			),
		)
	case *ast.MarkNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(sxpf.MakeString(n.Fragment)).
			Cons(sxpf.MakeString(n.Slug)).
			Cons(sxpf.MakeString(n.Mark)).
		return sxpf.NewPair(
			sexpr.SymMark,
			sxpf.NewPair(
				sxpf.NewString(n.Mark),
				sxpf.NewPair(
					sxpf.NewString(n.Slug),
					sxpf.NewPair(
						sxpf.NewString(n.Fragment),
			Cons(t.zetSyms.SymMark)
						t.getInlineSlice(n.Inlines),
					),
				),
			),
		)
	case *ast.FormatNode:
		return t.getInlineSlice(n.Inlines).Tail().
			Cons(t.getAttributes(n.Attrs)).
		return sxpf.NewPair(
			mapGetS(mapFormatKindS, n.Kind),
			sxpf.NewPair(
				getAttributes(n.Attrs),
			Cons(mapGetS(t, t.mapFormatKindS, n.Kind))
				t.getInlineSlice(n.Inlines),
			),
		)
	case *ast.LiteralNode:
		return sxpf.MakeList(
			mapGetS(t, t.mapLiteralKindS, n.Kind),
			t.getAttributes(n.Attrs),
			sxpf.MakeString(string(n.Content)),
		return sxpf.NewPairFromValues(
			mapGetS(mapLiteralKindS, n.Kind),
			getAttributes(n.Attrs),
			sxpf.NewString(string(n.Content)),
		)
	}
	log.Printf("SEXPR %T %v\n", node, node)
	return sxpf.MakeList(t.zetSyms.SymUnknown, sxpf.MakeString(fmt.Sprintf("%T %v", node, node)))
	return sxpf.NewPairFromValues(sexpr.SymUnknown, sxpf.NewString(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.List {
func (t *transformer) getRegion(rn *ast.RegionNode) *sxpf.Pair {
	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.MakeList(
		mapGetS(t, t.mapRegionKindS, rn.Kind),
		t.getAttributes(rn.Attrs),
	return sxpf.NewPairFromValues(
		mapGetS(mapRegionKindS, rn.Kind),
		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.List {
	nlistObjs := make([]sxpf.Object, len(ln.Items)+1)
	nlistObjs[0] = mapGetS(t, t.mapNestedListKindS, ln.Kind)
func (t *transformer) getNestedList(ln *ast.NestedListNode) *sxpf.Pair {
	nlistVals := make([]sxpf.Value, len(ln.Items)+1)
	nlistVals[0] = mapGetS(mapNestedListKindS, ln.Kind)
	isCompact := isCompactList(ln.Items)
	for i, item := range ln.Items {
		if isCompact && len(item) > 0 {
			paragraph := t.GetSexpr(item[0])
			nlistObjs[i+1] = paragraph.Tail().Cons(t.zetSyms.SymInline)
			paragraph := t.getSexpr(item[0])
			nlistVals[i+1] = paragraph.GetTail()
			continue
		}
		itemObjs := make([]sxpf.Object, len(item))
		itemVals := make([]sxpf.Value, len(item))
		for j, in := range item {
			itemObjs[j] = t.GetSexpr(in)
			itemVals[j] = t.getSexpr(in)
		}
		if isCompact {
			nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymInline)
		nlistVals[i+1] = sxpf.NewPairFromValues(itemVals...)
		} else {
			nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymBlock)
		}
	}
	}
	return sxpf.MakeList(nlistObjs...)
	return sxpf.NewPairFromValues(nlistVals...)
}
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.List {
	dlObjs := make([]sxpf.Object, 2*len(dn.Descriptions)+1)
	dlObjs[0] = t.zetSyms.SymDescription
func (t *transformer) getDescriptionList(dn *ast.DescriptionListNode) *sxpf.Pair {
	dlVals := make([]sxpf.Value, 2*len(dn.Descriptions)+1)
	dlVals[0] = sexpr.SymDescription
	for i, def := range dn.Descriptions {
		dlObjs[2*i+1] = t.getInlineSlice(def.Term)
		descObjs := make([]sxpf.Object, len(def.Descriptions))
		dlVals[2*i+1] = t.getInlineSlice(def.Term)
		descVals := make([]sxpf.Value, len(def.Descriptions))
		for j, b := range def.Descriptions {
			if len(b) == 1 {
				descVals[j] = t.getSexpr(b[0]).GetTail()
				continue
			}
			dVal := make([]sxpf.Object, len(b))
			dVal := make([]sxpf.Value, len(b))
			for k, dn := range b {
				dVal[k] = t.GetSexpr(dn)
				dVal[k] = t.getSexpr(dn)
			}
			descObjs[j] = sxpf.MakeList(dVal...).Cons(t.zetSyms.SymBlock)
			descVals[j] = sxpf.NewPairFromValues(dVal...)
		}
		dlObjs[2*i+2] = sxpf.MakeList(descObjs...).Cons(t.zetSyms.SymBlock)
		dlVals[2*i+2] = sxpf.NewPairFromValues(descVals...)
	}
	return sxpf.MakeList(dlObjs...)
	return sxpf.NewPairFromValues(dlVals...)
}

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)
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 {
		tObjs[i+2] = t.getRow(row)
		tVals[i+2] = t.getRow(row)
	}
	return sxpf.MakeList(tObjs...)
	return sxpf.NewPairFromValues(tVals...)
}
func (t *Transformer) getHeader(header ast.TableRow) *sxpf.List {
	if len(header) == 0 {
		return sxpf.Nil()
	}
	return t.getRow(header)
}
func (t *Transformer) getRow(row ast.TableRow) *sxpf.List {
	rObjs := make([]sxpf.Object, len(row))
func (t *transformer) getRow(row ast.TableRow) *sxpf.Pair {
	rVals := make([]sxpf.Value, len(row))
	for i, cell := range row {
		rObjs[i] = t.getCell(cell)
		rVals[i] = t.getCell(cell)
	}
	return sxpf.NewPairFromValues(rVals...)
}
	return sxpf.MakeList(rObjs...).Cons(t.zetSyms.SymList)

var alignmentSymbolS = map[ast.Alignment]*sxpf.Symbol{
	ast.AlignDefault: sexpr.SymCell,
	ast.AlignLeft:    sexpr.SymCellLeft,
	ast.AlignCenter:  sexpr.SymCellCenter,
	ast.AlignRight:   sexpr.SymCellRight,
}

func (t *Transformer) getCell(cell *ast.TableCell) *sxpf.List {
	return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align))
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.List {
	var lastObj sxpf.Object
func (t *transformer) getBLOB(bn *ast.BLOBNode) *sxpf.Pair {
	var lastValue sxpf.Value
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sxpf.MakeString(string(bn.Blob))
		lastValue = sxpf.NewString(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
		lastValue = getBase64String(bn.Blob)
	}
	return sxpf.MakeList(
		t.zetSyms.SymBLOB,
	return sxpf.NewPairFromValues(
		sexpr.SymBLOB,
		t.getInlineSlice(bn.Description),
		sxpf.MakeString(bn.Syntax),
		lastObj,
		sxpf.NewString(bn.Syntax),
		lastValue,
	)
}

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))
var mapRefStateLink = map[ast.RefState]*sxpf.Symbol{
	ast.RefStateInvalid:  sexpr.SymLinkInvalid,
	ast.RefStateZettel:   sexpr.SymLinkZettel,
	ast.RefStateSelf:     sexpr.SymLinkSelf,
	ast.RefStateFound:    sexpr.SymLinkFound,
}

func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.List {
	tail := t.getInlineSlice(en.Inlines).Tail()
	if en.Syntax == meta.SyntaxSVG {
	ast.RefStateBroken:   sexpr.SymLinkBroken,
	ast.RefStateHosted:   sexpr.SymLinkHosted,
	ast.RefStateBased:    sexpr.SymLinkBased,
	ast.RefStateQuery:    sexpr.SymLinkQuery,
	ast.RefStateExternal: sexpr.SymLinkExternal,
		tail = tail.Cons(sxpf.MakeString(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
}
	return tail.Cons(sxpf.MakeString(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB)
}


func (t *transformer) getLink(ln *ast.LinkNode) *sxpf.Pair {
func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sxpf.List {
	objs := make([]sxpf.Object, len(*bs))
	for i, n := range *bs {
		objs[i] = t.GetSexpr(n)
	}
	return sxpf.MakeList(objs...).Cons(t.zetSyms.SymBlock)
	return sxpf.NewPair(
}
func (t *Transformer) getInlineSlice(is ast.InlineSlice) *sxpf.List {
	objs := make([]sxpf.Object, len(is))
		mapGetS(mapRefStateLink, ln.Ref.State),
		sxpf.NewPair(
			getAttributes(ln.Attrs),
			sxpf.NewPair(
	for i, n := range is {
		objs[i] = t.GetSexpr(n)
				sxpf.NewString(ln.Ref.Value),
	}
	return sxpf.MakeList(objs...).Cons(t.zetSyms.SymInline)
				t.getInlineSlice(ln.Inlines),
}

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


func (t *transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.Pair {
	tail := t.getInlineSlice(en.Inlines)
	if en.Syntax == meta.SyntaxSVG {
		tail = sxpf.NewPair(sxpf.NewString(string(en.Blob)), tail)
	} else {
		tail = sxpf.NewPair(getBase64String(en.Blob), tail)
	}
func (t *Transformer) getReference(ref *ast.Reference) *sxpf.List {
	return sxpf.MakeList(
		t.zetSyms.SymQuote,
		sxpf.MakeList(
	return sxpf.NewPair(
		sexpr.SymEmbedBLOB,
		sxpf.NewPair(
			mapGetS(t, t.mapRefStateS, ref.State),
			sxpf.MakeString(ref.Value),
			getAttributes(en.Attrs),
			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))
	for i, n := range *bs {
		lstVals[i] = t.getSexpr(n)
	}
	return sxpf.NewPairFromSlice(lstVals)
}
func (t *transformer) getInlineSlice(is ast.InlineSlice) *sxpf.Pair {
	lstVals := make([]sxpf.Value, len(is))
	for i, n := range is {
		lstVals[i] = t.getSexpr(n)
	}
	return sxpf.NewPairFromSlice(lstVals)
}

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

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),
		sxpf.NewPair(
			sxpf.NewString(ref.Value),
			sxpf.Nil()))
}

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

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

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

Deleted 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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Added 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-present Detlef Stern
// 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 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
300
301
302
303
304
285
286
287
288
289
290
291





292

293
294
295
296
297
298
299







-
-
-
-
-
+
-







			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.Walk(v, dn)
			ast.WalkDescriptionSlice(v, b)
			}
		}
	}
}

var alignCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
346
347
348
349
350
351
352
353
354
355



356
357
358
359
360
361
362
341
342
343
344
345
346
347



348
349
350
351
352
353
354
355
356
357







-
-
-
+
+
+







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 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 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 escapeSeqs = strfun.NewSet(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==",
)

func (v *visitor) visitText(tn *ast.TextNode) {
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
390
391
392
393
394
395
396



397
398
399
400
401
402
403







-
-
-








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 {
		ast.Walk(v, &en.Inlines)
423
424
425
426
427
428
429
430

431
432
433
434
435
436
437
415
416
417
418
419
420
421

422
423
424
425
426
427
428
429







-
+







	}
	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.WriteByte(' ')
		v.b.WriteString(", ")
		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-present Detlef Stern
// Copyright (c) 2022-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.
//-----------------------------------------------------------------------------

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
29
30
31
32
33
34
35
36
37
38
39
40


41
42








1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53

-
+












+





+





















+
+

-
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
// Copyright (c) 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 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 {
		if val, found := m.Get(api.KeyPublished); found {
			if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil {
				if maxPublished.Before(published) {
					maxPublished = published
				}
			}
		}
	}
	if maxPublished.Year() > 1 {
		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 { return parser.NormalizedSpacedText(m.GetTitle()) }
func TitleAsText(m *meta.Meta) string {
	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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------

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

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

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-present Detlef Stern
// Copyright (c) 2022-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.
//-----------------------------------------------------------------------------
239
240
241
242
243
244
245





246
247
248
249
250


251
252
253
254
255
256
257
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253


254
255
256
257
258
259
260
261
262







+
+
+
+
+



-
-
+
+







	}
	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(m.Zid.String()),
			Inlines: parser.ParseSpacedText(m.GetTitle()),
			Ref:     ast.ParseReference(zid),
			Inlines: parser.ParseMetadataNoLink(title),
		})})
	}
	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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

Changes to go.mod.

1
2
3
4
5
6
7

8
9
10
11
12
13
14





15
16
17

1
2
3
4
5


6
7
8





9
10
11
12
13
14
15

16





-
-
+


-
-
-
-
-
+
+
+
+
+


-
+
module zettelstore.de/z

go 1.19

require (
	codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197
	codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475
	codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0
	github.com/fsnotify/fsnotify v1.6.0
	github.com/pascaldekloe/jwt v1.12.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
	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
)

require golang.org/x/sys v0.6.0 // indirect
require golang.org/x/sys v0.4.0 // indirect

Changes to go.sum.

1
2
3
4


5
6
7
8
9
10
11
12




13
14
15
16
17
18
19
20
21












1
2
3
4
5
6




7
8
9
10
11








12
13
14
15
16
17
18
19
-
-
-
-
+
+




-
-
-
-
+
+
+
+

-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
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-20230319111333-7de220f3b475 h1:0OTzV3FYY/Y7YsaVaSzF4Wd17pXzdH6DaSvMeqteJc4=
codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475/go.mod h1:iSbMygOmtRQYp8pryNKYzRuMibYDSR80smU2b6qm1bc=
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=
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.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=
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=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
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=

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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

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







-
-
-
-
+
+
+
+
+
















+







				case kernel.ConfigZmkHTML:
					return config.ZettelmarkupHTML, nil
				}
				return config.NoHTML, nil
			}),
			true,
		},
		api.KeyLang:         {"Language", parseString, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, 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},
		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,
	}
}
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
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







-
+


-
+




















+
+
+
-
+








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

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

func (cs *configService) doUpdate(p box.BaseBox) error {
func (cs *configService) doUpdate(p box.Box) 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) }()
	if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid {
	} else if 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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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

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

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

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

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

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

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

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

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

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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 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 sb strings.Builder
		_, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
		var buf bytes.Buffer
		_, err := cv.textEnc.WriteInlines(&buf, &hn.Inlines)
		if err != nil {
			return
		}
		hn.Slug = strfun.Slugify(sb.String())
		hn.Slug = strfun.Slugify(buf.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-present Detlef Stern
// Copyright (c) 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.
//

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

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

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

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

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

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

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-present Detlef Stern
// Copyright (c) 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.
//
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 {
	var sb strings.Builder
	strfun.XMLEscape(&sb, s)
	return sb.String()
	b := bytes.Buffer{}
	strfun.XMLEscape(&b, s)
	return b.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-present Detlef Stern
// Copyright (c) 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.
//

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

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

18
19
20
21
22
23
24

-
+















-







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 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"
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
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







-
+







-
-
+
+






-
-
+
+





-
+

-
+







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

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

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


442
443
444
445

446
447
448

449
450
451
452
453
454
455
432
433
434
435
436
437
438


439
440
441
442
443

444
445
446

447
448
449
450
451
452
453
454







-
-
+
+



-
+


-
+







			Attrs:   a,
		},
	}
}

func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice {
	is := p.acceptInlineChildren(node)
	var sb strings.Builder
	_, err := p.textEnc.WriteInlines(&sb, &is)
	var buf bytes.Buffer
	_, err := p.textEnc.WriteInlines(&buf, &is)
	if err != nil {
		panic(err)
	}
	if sb.Len() == 0 {
	if buf.Len() == 0 {
		return nil
	}
	return ast.InlineSlice{&ast.TextNode{Text: sb.String()}}
	return ast.InlineSlice{&ast.TextNode{Text: buf.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-present Detlef Stern
// 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 markdown

import (
	"strings"
	"bytes"
	"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 sb strings.Builder
		var buf bytes.Buffer
		for _, in := range splitText(tc.text) {
			switch n := in.(type) {
			case *ast.TextNode:
				sb.WriteByte('T')
				sb.WriteString(n.Text)
				buf.WriteByte('T')
				buf.WriteString(n.Text)
			case *ast.SpaceNode:
				sb.WriteByte('S')
				sb.WriteString(n.Lexeme)
				buf.WriteByte('S')
				buf.WriteString(n.Lexeme)
			default:
				sb.WriteByte('Q')
				buf.WriteByte('Q')
			}
		}
		got := sb.String()
		got := buf.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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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
24
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-present Detlef Stern
// 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 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"
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
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







-
-
-
-
+
+
+
+
+
+

-
-
-






-
-
+
+
+
-
-

-
-
+
+

-
+








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

// ParseSpacedText returns an inline slice that consists just of test and space node.
// No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice.
func ParseSpacedText(s string) ast.InlineSlice {
	return ast.CreateInlineSliceFromWords(meta.ListFromValue(s)...)
// 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 {
	in := ParseMetadata(value)
	cleaner.CleanInlineLinks(&in)
	return in
}

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

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

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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
1

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

-
+












+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 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"
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
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







-
+


-
+






-
+

-
+





-
+

-
-
+
+

-
+






-
+

-
+



-
+

-
+

-
+


-
+


-
-
+
+

-
+


-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+


-
+

-
+

-
+

-
-
+
+

-
+

-
+


-
+

-
+


-
+

-
-
+
+

-
+

-
+


-
+

-
+


-
-
-
+
+
+

-
+


-
+

-
+



-
+

-
+


-
+

-
+


-
+



-
+




-
+



-
+


-
+

-
+


-
+

-
-
-
+
+
+


-
-
+
+




-
+

-
+

-
+






-
-
+
+

-
-
+
+

-
+







	})
}

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

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

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

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

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







-
+








-
+


-
-
+
+


-
+

-
-
-
+
+
+

-
+




-
+










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

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

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

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

	tv.sb.WriteByte(']')
	tv.buf.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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
62
63
64
65
66
67
68

69
70
71
72
73
74
75







-







	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 {
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
84
85
86
87
88
89
90

91
92
93
94
95







96
97
98
99
100
101
102







-
+




-
-
-
-
-
-
-







				q.terms = append(q.terms, conjTerms{})
			}
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(kwRandom) {
			q = createIfNeeded(q)
			if len(q.order) == 0 && q.pick <= 0 {
			if len(q.order) == 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
			}
		}
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
120
121
122
123
124
125
126















127
128
129
130
131
132
133







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







			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()
	if len(word) == 0 {

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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
39
40
41
42
43
44
45

46
47
48
49
50
51
52







-







		{`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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
63
64
65
66
67
68
69

70
71
72
73
74
75
76







-







		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 {
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
168
169
170
171
172
173
174

175
176
177
178
179
180
181







-







			}
			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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
43
44
45
46
47
48
49


50
51
52
53
54
55
56







-
-







	// 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
	actions []string
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
142
143
144
145
146
147
148

149
150
151
152
153
154
155







-







		}
		// }
		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
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
374
375
376
377
378
379
380

381



382
383
384
385
386

387



388


389



390
391
392
393
394
395
396
397







-
+
-
-
-
+




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







	}
	negatives := retrieveNegatives(negCalls)
	return func(zid id.Zid) bool {
		return positives.Contains(zid) && !negatives.Contains(zid)
	}
}

// AfterSearch applies all terms to the metadata list that was searched.
// Sort applies the sorter to the slice of meta data.
//
// This includes sorting, offset, limit, and picking.
func (q *Query) AfterSearch(metaList []*meta.Meta) []*meta.Meta {
func (q *Query) Sort(metaList []*meta.Meta) []*meta.Meta {
	if len(metaList) == 0 {
		return metaList
	}

	if q == nil {
	if q == nil || len(q.order) == 0 {
		return sortMetaByZid(metaList)
	}

		sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
	metaList = q.doPick(metaList)

		if q == nil {
	if len(q.order) == 0 {
		if q.pick <= 0 {
			metaList = sortMetaByZid(metaList)
			return metaList
		}
	} else if q.order[0].isRandom() {
		metaList = q.sortRandomly(metaList)
	} else {
		sort.Slice(metaList, createSortFunc(q.order, metaList))
	}

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












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

-
-
-
-
-
-

+













-
-
+
+



-
-
-
-
-

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 limit := q.limit; limit > 0 && limit < len(metaList) {
		return metaList[:limit]
	if q.limit > 0 && q.limit < len(metaList) {
		return metaList[:q.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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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
1

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

-
+











+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 query

import (
	"bytes"
	"fmt"
	"strings"
	"unicode/utf8"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
273
274
275
276
277
278
279
280
281


282
283
284

285
286
287
288
289
290
291
274
275
276
277
278
279
280


281
282
283
284

285
286
287
288
289
290
291
292







-
-
+
+


-
+







			if !hasUpper {
				return zmk
			}
			return strings.ToLower(zmk)
		}
	}
	is := parser.ParseMetadata(zmk)
	var sb strings.Builder
	if _, err := textenc.Create().WriteInlines(&sb, &is); err != nil {
	var buf bytes.Buffer
	if _, err := textenc.Create().WriteInlines(&buf, &is); err != nil {
		return strings.ToLower(zmk)
	}
	return strings.ToLower(sb.String())
	return strings.ToLower(buf.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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
48
49
50
51
52
53
54

























































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







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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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
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-present Detlef Stern
// Copyright (c) 2021-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 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)
31
32
33
34
35
36
37
38

39
40

41
42
43

44
45

46
47
48
49
50
51
52
53
32
33
34
35
36
37
38

39
40

41
42
43

44
45

46
47
48
49
50
51
52
53
54







-
+

-
+


-
+

-
+








		runes = append(runes, r)
	}
	if len(runes) > maxLen {
		runes = runes[:maxLen]
		runes[maxLen-1] = '\u2025'
	}

	var sb strings.Builder
	var buf bytes.Buffer
	for _, r := range runes {
		sb.WriteRune(r)
		buf.WriteRune(r)
	}
	for i := 0; i < maxLen-len(runes); i++ {
		sb.WriteRune(pad)
		buf.WriteRune(pad)
	}
	return sb.String()
	return buf.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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//
24
25
26
27
28
29
30
31

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

31

32
33
34
35
36
37
38







-
+
-







import (
	"fmt"
	"io"
	"reflect"
	"regexp"
	"strings"

	"codeberg.org/t73fde/sxhtml"
	"zettelstore.de/c/html"
	"codeberg.org/t73fde/sxpf"
)

// Node represents a node in the parse tree.
// It is either a Tag or a textNode.
type node interface {
	node()
}
596
597
598
599
600
601
602
603

604
605
606
607
608
609
610
595
596
597
598
599
600
601

602
603
604
605
606
607
608
609







-
+







		if err != nil {
			return err
		}
		if val.IsValid() {
			if n.raw {
				fmt.Fprint(w, val.Interface())
			} else {
				sxhtml.NewGenerator(nil).WriteHTML(w, sxpf.MakeString(fmt.Sprint(val.Interface())))
				html.Escape(w, 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-present Detlef Stern
// 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.
//
17
18
19
20
21
22
23

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







+







// 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"
)
207
208
209
210
211
212
213
214
215
216



217
218
219
220
221
222
223
208
209
210
211
212
213
214



215
216
217
218
219
220
221
222
223
224







-
-
-
+
+
+







}

func parseString(data string) (*template.Template, error) {
	return template.ParseString(data, nil)
}

func render(tmpl *template.Template, data interface{}) (string, error) {
	var sf strings.Builder
	err := tmpl.Render(&sf, data)
	return sf.String(), err
	var buf bytes.Buffer
	err := tmpl.Render(&buf, data)
	return buf.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-present Detlef Stern
// 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.
//

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-present Detlef Stern
// Copyright (c) 2021-2022 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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181







+







}

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







-
-
-
+
+
+










-
-
+
+







	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, 1, creatorZid)
	checkListZid(t, l, 1, writerZid)
	checkListZid(t, l, 2, readerZid)
	// checkListZid(t, l, 3, 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, exp := len(l), 4; got != exp {
		t.Errorf("Expected list of length %d, got %d", exp, got)
	if got := len(l); got != 1 {
		t.Errorf("Expected list of length 1, got %d", 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-present Detlef Stern
// Copyright (c) 2021-2022 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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------
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 := "", string(content); exp != got {
	if exp, got := "<p></p>", 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
37
1

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

18
19
20
21
22
23
24
25
26
27


28
29
30
31
32
33
34
35
36

-
+















-










-
-
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 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/shtmlenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zjsonenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	_ "zettelstore.de/z/parser/markdown"
	_ "zettelstore.de/z/parser/zettelmark"
)

78
79
80
81
82
83
84
85

86
87
88
89
90


91
92
93
94
95
96
97
77
78
79
80
81
82
83

84
85
86
87


88
89
90
91
92
93
94
95
96







-
+



-
-
+
+







}

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 sb strings.Builder
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc).WriteBlocks(&sb, ast)
			sb.Reset()
			encoder.Create(enc).WriteBlocks(&buf, ast)
			buf.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk)
	var buf bytes.Buffer
127
128
129
130
131
132
133
134

135
136
137
138
139



140
141
142
143
144
126
127
128
129
130
131
132

133
134
135



136
137
138
139
140
141
142
143







-
+


-
-
-
+
+
+





	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, `abc@@<br>@@{="html"}def`},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk)
	var sb strings.Builder
	var buf bytes.Buffer
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		sb.Reset()
		zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &ast)
		got := buf.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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

-
+












+






-



















+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 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)
117
118
119
120
121
122
123
124
125
126



127
128
129
130
131
132
133
118
119
120
121
122
123
124



125
126
127
128
129
130
131
132
133
134







-
-
-
+
+
+







	return u.Path[len(root):]
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
	t.Helper()

	if enc := encoder.Create(enc); enc != nil {
		var sf strings.Builder
		enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, sf.String())
		var buf bytes.Buffer
		enc.WriteMeta(&buf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, buf.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
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

-
+













+



















-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
// Copyright (c) 2021-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 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 strings.Builder
	var out bytes.Buffer
	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
140
141
142
143
144
127
128
129
130
131
132
133





134
135
136
137
138
139
140







-
-
-
-
-







	}
	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...)
	out, err := executeCommand(directProxy, "go", args...)
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
205
206
207
208
209
210
211











212
213
214
215
216
217
218







-
-
-
-
-
-
-
-
-
-
-







				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")
}
249
250
251
252
253
254
255
256

257
258
259
260
261
262
263
234
235
236
237
238
239
240

241
242
243
244
245
246
247
248







-
+







		fmt.Fprintln(os.Stderr)
	}
	return nil
}

type zsInfo struct {
	cmd          *exec.Cmd
	out          strings.Builder
	out          bytes.Buffer
	adminAddress string
}

func cmdTestAPI() error {
	var err error
	var info zsInfo
	needServer := !addressInUse(":23123")
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
269
270
271
272
273
274
275

276
277
278
279
280
281
282







-







		"-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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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
32
33
34
35
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) 2021-present Detlef Stern
// Copyright (c) 2021-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 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
}
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
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







-
-
-
-
-

















-















-







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







-
+



-
+







+




-
-
+
-







func contextCost(key string) int {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 1
	case api.KeySuccessors, api.KeyPredecessor:
		return 2
	}
	return 3
	return 5
}

func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, newCost int, value string) {
	if zc.costMaxed(newCost) {
	if (zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit() {
		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
	}

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
190
191
192
193
194
195
196




197




198
199
200















































201
202
203
204
205
206
207







-
-
-
-

-
-
-
-
+
+

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








	// 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 elems {
		zc.addID(ctx, port, refCost, val)
	for _, val := range meta.ListFromValue(value) {
		zc.addID(ctx, port, newCost, 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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------
68
69
70
71
72
73
74







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







+
+
+
+
+
+
+








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

// GetZettel retrieves the full zettel of a given zettel identifier.

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------
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.WriteData(w, buf.Bytes(), contentType)
	return adapter.WriteBytes(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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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

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

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

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
1

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

-
+








+







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

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

20
21
22
23
24
25
26

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


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

-
+













+



-
+






-
+
+
+
+
+
















-
-
+
+
+
+
+
+
+
+







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

import (
	"bytes"
	"net/http"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/encoder/textenc"
	"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(getMeta usecase.GetMeta, unlinkedRefs usecase.UnlinkedReferences) http.HandlerFunc {
func (a *API) MakeListUnlinkedMetaHandler(
	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 title, found := zm.Get(api.KeyTitle); found {
				phrase = parser.NormalizedSpacedText(title)
			if zmkTitle, 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()
				}
			}
		}

		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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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 adapter

import (
	"errors"
	"fmt"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// WriteData emits the given data to the response writer.
func WriteData(w http.ResponseWriter, data []byte, contentType string) error {
// WriteBytes emits the given data to the response writer.
func WriteBytes(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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
47
48
49
50
51
52
53
54

55
56

57
58

59
60
61









62
63
64
65
66
67
68
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







-
+

-
+

-
+

-
-
+
+
+
+
+
+
+
+
+







			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", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "Copy Zettel", "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "Versionzettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "Folgezettel", "", roleData, syntaxData)
		case actionNew:
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), title, "", roleData, syntaxData)
			m := origZettel.Meta
			title := parser.ParseMetadata(m.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)
		}
	}
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
	roleData := dataListFromArrangement(ucListRoles.Run(ctx))
	syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx))
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
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







-
+









-
+







	return nil
}

func (wui *WebUI) renderZettelForm(
	ctx context.Context,
	w http.ResponseWriter,
	zettel domain.Zettel,
	title string,
	title, heading 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:       title,
		Heading:       heading,
		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,
165
166
167
168
169
170
171
172

173
174
172
173
174
175
176
177
178

179
180
181







-
+


		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", wui.createNewURL, roleData, syntaxData)
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", "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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
21
22
23
24
25
26
27
28





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

50
51
52
53
54
55
56
21
22
23
24
25
26
27

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

53
54
55
56
57
58
59
60







-
+
+
+
+
+




















-
+







	"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(getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta) http.HandlerFunc {
func (wui *WebUI) MakeGetDeleteZettelHandler(
	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(ctx, getMeta)
			getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate))
			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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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", "", roleData, syntaxData)
		wui.renderZettelForm(ctx, w, zettel, "Edit 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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------
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.WriteData(w, data, "")
		err = adapter.WriteBytes(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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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

20
21
22
23
24
25
26

27
28
29
30
31
32
33

-
+











+





-







-







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 (
	"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
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
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







+



-
+

-
-
-
+
+
+




-
+
-
-
-
+









-
+


-
+








		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(ctx, getMeta)
		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata)
		for i, p := range pairs {
			var sb strings.Builder
			if sval := wui.writeHTMLMetaValue(
				p.Key, p.Value,
			var buf bytes.Buffer
			wui.writeHTMLMetaValue(
				&buf, p.Key, p.Value,
				getTextTitle,
				func(val string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, val)
				},
				enc); !sxpf.IsNil(sval) {
				enc)
				wui.htmlGen.WriteHTML(&sb, sval)
			}
			metaData[i] = metaDataInfo{p.Key, sb.String()}
			metaData[i] = metaDataInfo{p.Key, buf.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()
		}

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext)
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = title
			phrase = textTitle
		}
		phrase = strings.TrimSpace(phrase)
		unlinkedMeta, err := unlinkedRefs.Run(
			ctx, phrase, adapter.AddUnlinkedRefsToQuery(nil, zn.InhMeta))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
114
115
116
117
118
119
120
121

122
123
124
125
126
127
128
112
113
114
115
116
117
118

119
120
121
122
123
124
125
126







-
+







			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), title, "", user, &base)
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, "", 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
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-present Detlef Stern
// 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 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/parser"
	"zettelstore.de/z/encoder/textenc"
	"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) {
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
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







-
+
+
+
+

















-
-
+
+




-

-
-
-
+
+

-
+


















+







-
+


















+










-
+




-
+


-
+
-
-
+



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

















+
-
+
-
-
-
+







		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))

		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder()
		enc := wui.createZettelEncoder(ctx, zn.InhMeta)
		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(ctx, getMeta)
		extURL, hasExtURL := wui.formatURLFromMeta(zn.Meta, api.KeyURL)
		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata)
		extURL, hasExtURL := 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), title, roleCSSURL, user, &base)
		base.MetaHeader = enc.MetaString(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, roleCSSURL, user, &base)
		base.MetaHeader = enc.MetaString(zn.InhMeta, evalMetadata)
		wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
			Heading         string
			HTMLTitle       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
		}{
			Heading:         title,
			HTMLTitle:       htmlTitle,
			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 (wui *WebUI) formatURLFromMeta(m *meta.Meta, key string) (string, bool) {
func formatURLFromMeta(m *meta.Meta, key string) (string, bool) {
	val, found := m.Get(key)
	if !found {
		return "", false
	}
	if val == "" {
	if found && val == "" {
		return "", false
	}

	var buf bytes.Buffer
	var sb strings.Builder
	_, err := wui.htmlGen.WriteHTML(&sb, wui.transformURL(val))
	_, err := html.AttributeEscape(&buf, val)
	if err != nil {
		return "", false
	}
	return sb.String(), true
	return buf.String(), true
}

func encodeInlinesText(is *ast.InlineSlice, enc *textenc.Encoder) (string, error) {
	if is == nil || len(*is) == 0 {
		return "", nil
	}

	var buf bytes.Buffer
	_, err := enc.WriteInlines(&buf, is)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}

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
		sval := wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
		wui.writeIdentifierSet(&buf, meta.ListFromValue(value), getTextTitle)
		var sb strings.Builder
		wui.htmlGen.WriteHTML(&sb, sval)
		return sb.String()
		return buf.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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

-
+











-
+


-


-
+
-

-




+
+










-
-
-
+
+
+
+


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








-
-
-
-
-
-

+
-
+

-
+



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


-
-
-
+
+
+

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

-
-
+
+


-
+

-
+

-
+
-
-
-
-







-
-
-
-
+
+
+
+
+
+

-
-
-
+
-
-
-
+
+

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

-
+


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

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

import (
	"net/url"
	"bytes"
	"strings"

	"codeberg.org/t73fde/sxhtml"
	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/html"
	"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 {
	tx    *sexprenc.Transformer
	th    *shtml.Transformer
	symAt *sxpf.Symbol
	builder   urlBuilder
	textEnc   *textenc.Encoder
	extMarker string
	env       *html.EncEnvironment
}

func (wui *WebUI) createGenerator(builder urlBuilder) *htmlGenerator {
	th := shtml.NewTransformer(1, wui.sf)
	symA := th.Make("a")
func createGenerator(builder urlBuilder, extMarker string) *htmlGenerator {
	env := html.NewEncEnvironment(nil, 1)
	gen := &htmlGenerator{
	symImg := th.Make("img")
	symAt := th.Make(sxhtml.NameSymAttr)

		builder:   builder,
	symHref := th.Make("href")
	symClass := th.Make("class")
	symTarget := th.Make("target")
	symRel := th.Make("rel")

		textEnc:   textenc.Create(),
	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
		}

		extMarker: extMarker,
		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)
	}

		env:       env,
	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()
			}

			attr, assoc, rest := findA(obj)
			if attr == nil {
				return obj
			}
	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))

			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 {
	f, err := env.Builtins.LookupForm(sexpr.SymEmbed)
	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 {
		panic(err)
				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)
	b := f.(*sxpf.Builtin)
			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{
	env.Builtins.Set(sexpr.SymEmbed, sxpf.NewBuiltin(b.Name(), true, 3, -1, gen.makeGenerateEmbed(b.GetValue())))
	return gen
		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 {
		metaMap[api.KeyTags] = g.transformMetaTags(tags)
		writeMetaTags(&buf, 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
		}
		a := make(attrs.Attributes, 32)
	for _, p := range m.ComputedPairs() {
		for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() {
			if p, ok2 := aelem.Car().(*sxpf.List); ok2 {
				key := p.Car()
		key := p.Key
				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) {
		if ignore.Has(key) {
			continue
		}
		var newName string
		if altName, found2 := mapMetaKey[name]; found2 {
			newName = altName
		if altKey, found := mapMetaKey[key]; found {
			buf.WriteString(`<meta name="`)
			buf.WriteString(altKey)
		} else {
			newName = "zs-" + name
			buf.WriteString(`<meta name="zs-`)
		}
		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-- {
			buf.WriteString(key)
		result = result.Cons(metaMap[keys[i]])
	}

	var sb strings.Builder
	gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(result))
	_, _ = gen.WriteListHTML(&sb, result)
	return sb.String()
		}
		buf.WriteString(`" content="`)
		is := evalMeta(p.Value)
		var sb strings.Builder
		g.textEnc.WriteInlines(&sb, &is)
		html.AttributeEscape(&buf, sb.String())
		buf.WriteString("\">\n")
	}
	return buf.String()
}
func (g *htmlGenerator) transformMetaTags(tags string) *sxpf.List {
	var sb strings.Builder
func writeMetaTags(buf *bytes.Buffer, tags string) {
	buf.WriteString(`<meta name="keywords" content="`)
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			sb.WriteString(", ")
			buf.WriteString(", ")
		}
		sb.WriteString(strings.TrimPrefix(val, "#"))
		html.AttributeEscape(buf, strings.TrimPrefix(val, "#"))
	}
	metaTags := sb.String()
	buf.WriteString("\">\n")
	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
	}
	sx := g.tx.GetSexpr(bs)
	sh, err := g.th.Transform(sx)
	if err != nil {
		return "", err
	lst := sexprenc.GetSexpr(bs)
	var buf bytes.Buffer
	g.env.ReplaceWriter(&buf)
	sxpf.Eval(g.env, lst)
	if g.env.GetError() == nil {
		g.env.WriteEndnotes()
	}
	var sb strings.Builder
	gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(sh), sxhtml.WithNewline)
	_, err = gen.WriteListHTML(&sb, sh)
	g.env.ReplaceWriter(nil)
	if err != nil {
		return sb.String(), err
	}
	return buf.String(), g.env.GetError()
}

// InlinesString writes an inline slice to the writer
	_, err = gen.WriteHTML(&sb, g.th.Endnotes())
	return sb.String(), err
}

// 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)
	sh, err := g.th.Transform(sx)
func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) string {
	if is == nil || len(*is) == 0 {
		return ""
	}
	return html.EvaluateInline(g.env, sexprenc.GetSexpr(is), true, false)
}

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)
		}
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	}
	return nil, nil
}

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)
	if err != nil {
		return sxpf.Nil()
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	}
	return sh
	return nil, nil
}

func (g *htmlGenerator) generateLinkQuery(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		queryExpr := query.Parse(refValue).String()
		u := g.builder.NewURLBuilder('h').AppendQuery(queryExpr)
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	}
	return nil, nil
}
// InlinesString returns an inline slice, encoded as a HTML string.
func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) string {
	if sh := g.InlinesSxHTML(is); !sxpf.IsNil(sh) {
		var sb strings.Builder
		gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(sh))
		_, _ = gen.WriteListHTML(&sb, sh)
		return sb.String()
	}
	return ""

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)
	}
	return nil, nil
}

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

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

194
195
196
197

198
199

200
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

-
+














+



-
-

+




-



+
+

+




-
+
-


-
+

-
+

-
+

-
+

-
+

-
+

-
+


-
+


-
+

-
+

-
+

-
+

+
-
+

-


+
+
+
-
+
-


+
-
+





-
-
-
+
+
+
+

-
-

-
-
-
+
+
+



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

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


-
+
-

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


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

-


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










-
+

-
+






-
+



-
+

-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 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:
		sval = sxpf.MakeString(value)
		writeCredential(w, value)
	case meta.TypeEmpty:
		sval = sxpf.MakeString(value)
		writeEmpty(w, value)
	case meta.TypeID:
		sval = wui.transformIdentifier(value, getTextTitle)
		wui.writeIdentifier(w, value, getTextTitle)
	case meta.TypeIDSet:
		sval = wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
		wui.writeIdentifierSet(w, meta.ListFromValue(value), getTextTitle)
	case meta.TypeNumber:
		sval = wui.transformLink(key, value, value)
		wui.writeNumber(w, key, value)
	case meta.TypeString:
		sval = sxpf.MakeString(value)
		writeString(w, value)
	case meta.TypeTagSet:
		sval = wui.transformTagSet(key, meta.ListFromValue(value))
		wui.writeTagSet(w, key, meta.ListFromValue(value))
	case meta.TypeTimestamp:
		if ts, ok := meta.TimeValue(value); ok {
			sval = wui.transformTimestamp(ts)
			writeTimestamp(w, ts)
		}
	case meta.TypeURL:
		sval = wui.transformURL(value)
		writeURL(w, value)
	case meta.TypeWord:
		sval = wui.transformWord(key, value)
		wui.writeWord(w, key, value)
	case meta.TypeWordSet:
		sval = wui.transformWordSet(key, meta.ListFromValue(value))
		wui.writeWordSet(w, key, meta.ListFromValue(value))
	case meta.TypeZettelmarkup:
		sval = wui.transformZmkMetadata(value, evalMetadata, gen)
		io.WriteString(w, encodeZmkMetadata(value, evalMetadata, gen))
	default:
		html.Escape(w, value)
		sval = sxpf.Nil().Cons(sxpf.MakeString(fmt.Sprintf(" <b>(Unhandled type: %v, key: %v)</b>", kt, key))).Cons(wui.sf.MustMake("b"))
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
	}
	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) transformIdentifier(val string, getTextTitle getTextTitleFunc) sxpf.Object {
func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTextTitle getTextTitleFunc) {
	text := sxpf.MakeString(val)
	zid, err := id.Parse(val)
	if err != nil {
		html.Escape(w, val)
		return text
		return
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
		attrs := sxpf.Nil()
		if title != "" {
			attrs = attrs.Cons(sxpf.Cons(wui.sf.MustMake("title"), sxpf.MakeString(title)))
		if title == "" {
			fmt.Fprintf(w, "<a href=\"%v\">%v</a>", ub, zid)
		} else {
			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:
		return sxpf.Nil().Cons(text).Cons(wui.sf.MustMake("s"))
	default: // case found < 0:
		return text
		fmt.Fprintf(w, "<s>%v</s>", val)
	case found < 0:
		io.WriteString(w, val)
	}
}

func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) sxpf.Object {
	if len(vals) == 0 {
		return sxpf.Nil()
	}
func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTextTitle getTextTitleFunc) {
	for i, val := range vals {
		if i > 0 {
			w.Write(space)
		}
	space := sxpf.MakeString(" ")
	text := make([]sxpf.Object, 0, 2*len(vals))
	for _, val := range vals {
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
		wui.writeIdentifier(w, 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 (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))
	for _, tag := range tags {
		text = append(text, space, wui.transformLink(key, tag, tag))
	}

func writeString(w io.Writer, val string) { html.Escape(w, val) }

func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) {
	for i, tag := range tags {
		if i > 0 {
			w.Write(space)
		}
	return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span"))
}

func (wui *WebUI) transformTimestamp(ts time.Time) sxpf.Object {
		wui.writeLink(w, key, tag, tag)
	}
}

	return sxpf.MakeList(
		wui.sf.MustMake("time"),
func writeTimestamp(w io.Writer, ts time.Time) {
	io.WriteString(w, `<time datetime="`)
		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, ts.Format("2006-01-02T15:04:05"))
		),
		sxpf.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sxpf.MakeString(ts.Format("2006-01-02&nbsp;15:04:05"))),
	io.WriteString(w, `">`)
	io.WriteString(w, ts.Format("2006-01-02&nbsp;15:04:05"))
	)
	io.WriteString(w, `</time>`)
}

func (wui *WebUI) transformURL(val string) sxpf.Object {
func writeURL(w io.Writer, val string) {
	text := sxpf.MakeString(val)
	u, err := url.Parse(val)
	if err == nil {
		if us := u.String(); us != "" {
	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),
		io.WriteString(w, `<a href="`)
		html.AttributeEscape(w, us)
					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,
		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) {
func (wui *WebUI) transformWord(key, word string) sxpf.Object {
	return wui.transformLink(key, word, word)
	wui.writeLink(w, key, word, word)
}

func (wui *WebUI) transformWordSet(key string, words []string) sxpf.Object {
	if len(words) == 0 {
		return sxpf.Nil()
	}
func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) {
	for i, word := range words {
		if i > 0 {
			w.Write(space)
		}
	space := sxpf.MakeString(" ")
	text := make([]sxpf.Object, 0, 2*len(words))
	for _, tag := range words {
		text = append(text, space, wui.transformWord(key, tag))
		wui.writeWord(w, key, word)
	}
	return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span"))
}

func (wui *WebUI) transformLink(key, value, text string) sxpf.Object {
func (wui *WebUI) writeLink(w io.Writer, key, value, text string) {
	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())),
	fmt.Fprintf(w, `<a href="%v">`, wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value))
		),
		sxpf.MakeString(text),
	)
	html.Escape(w, text)
	io.WriteString(w, "</a>")
}

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(ctx context.Context, getMeta usecase.GetMeta) getTextTitleFunc {
func (wui *WebUI) makeGetTextTitle(getMetadata getMetadataFunc, evalMetadata evalMetadataFunc) getTextTitleFunc {
	return func(zid id.Zid) (string, int) {
		m, err := getMeta.Run(box.NoEnrichContext(ctx), zid)
		m, err := getMetadata(zid)
		if err != nil {
			if errors.Is(err, &box.ErrNotAllowed{}) {
				return "", -1
			}
			return "", 0
		}
		return parser.NormalizedSpacedText(m.GetTitle()), 1
		return encodeEvaluatedTitleText(m, evalMetadata, wui.gentext), 1
	}
}

func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sxpf.Object {
func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) string {
	is := evalMetadata(value)
	return gen.InlinesSxHTML(&is).Cons(wui.sf.MustMake("span"))
	return gen.InlinesString(&is)
}

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
1

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

-
+











+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
// 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 webui

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"net/url"
	"strings"

	"zettelstore.de/c/api"
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
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83

84
85
86
87
88
89
90







-









-







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

193
194
195
196
197
198
199
200
201
202



203
192
193
194
195
196
197
198



199
200
201
202







-
-
-
+
+
+

	return val
}

func (wui *WebUI) listTitleQuery(q *query.Query) string {
	if q == nil {
		return wui.rtConfig.GetSiteName()
	}
	var sb strings.Builder
	q.PrintHuman(&sb)
	return sb.String()
	var buf bytes.Buffer
	q.PrintHuman(&buf)
	return buf.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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------
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) http.HandlerFunc {
func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, 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
		}

		m, err := getMeta.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		getTextTitle := wui.makeGetTextTitle(ctx, getMeta)
		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate))
		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-present Detlef Stern
// Copyright (c) 2021-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.
//-----------------------------------------------------------------------------

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

-
+












+






-
-








+
+







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







+
+




















-
-
-















-











+
+














-
-
-







	authz    auth.AuthzManager
	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) {
316
317
318
319
320
321
322
323




324
325
326
327
328
329
330
314
315
316
317
318
319
320

321
322
323
324
325
326
327
328
329
330
331







-
+
+
+
+







	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 wui.createGenerator(wui) }
func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return 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"`
	}
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
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







+
+
+

-
+










-
-
-
+
+
+
+







		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: parser.NormalizedSpacedText(m.GetTitle()),
			Text: menuTitle,
			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 := wui.getSimpleHTMLEncoder()
			if result, err3 := htmlEnc.BlocksString(&zn.Ast); err3 == nil {
				return result
			htmlEnc := encoder.Create(api.EncoderHTML)
			var buf bytes.Buffer
			if _, err2 = htmlEnc.WriteBlocks(&buf, &zn.Ast); err2 == nil {
				return buf.String()
			}
		}
	}
	return ""
}

func (wui *WebUI) renderTemplate(
421
422
423
424
425
426
427
428

429
430
431
432
433
434
435
426
427
428
429
430
431
432

433
434
435
436
437
438
439
440







-
+







		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 strings.Builder
	var content bytes.Buffer
	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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------
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-present Detlef Stern
// Copyright (c) 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

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-present Detlef Stern
// 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.
//-----------------------------------------------------------------------------

Changes to www/build.md.

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








-







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


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

-
+
-
-
-
-
-



















-
+







<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 id="0_11"></a>
<a name="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>
<h2>Changes for Version 0.11.0 (pending)</h2>
  *  Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
     encoding instead.
     (breaking)
  *  Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
     just a plain string, possibly empty. Therefore, no inline formatting (like
     bold text), no links, no footnotes, no citations (the latter made
     rendering the title often questionable, in some contexts). If you used
     special entities, please use the unicode characters directly. However, as
     a good practice, it is often the best to printable ASCII characters.
     (breaking)
  *  Remove runtime configuration <tt>marker-external</tt>. It was added in
     version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
     change the marker for an external URL, you could modify zettel
     00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
     (Zettelstore User CSS, preferred) by changing / adding a rule to add some
     content after an external <tt><a ...></tt> tag.
     (breaking: webui)
  *  Add SHTML encoding. This allows to ensure the quality of generated HTML
     code. In addition, clients might use it, because it is easier to parse and
     manipulate than ordinary HTML. In the future, HTML template zettel will
     probably also use SHTML, deprecating the current Mustache syntax (which
     was added in [#0_0_9|0.0.9]).
     (major)
  *  Search term <tt>PICK n</tt>, where <tt>n</tt> is an integer value greater
     zero, will pick randomly <tt>n</tt> elements from the search result list.
     Somehow similar (and faster) as <tt>RANDOM LIMIT n</tt>, but allows also
     later ordering of the resulting list.
     (minor)
  *  Changed cost model for zettel context: a zettel with more
     outgoing/incoming references has higher cost than a zettel with less
     references. Also added support for traversing tags, with a similar cost
     model. As an effect, zettel hubs (in many cases your home zettel) will
     less likely add its references. Same for often used tags. The cost model
     might change in some details in the future, but the idea of a penalty
     applied to zettel / tags with many references will hold.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_10"></a>
<a name="0_10"></a>
<h2>Changes for Version 0.10.1 (2023-01-30)</h2>
  *  Show button to save a query into a zettel only when the current user has
     authorization to do it.
     (fix: webui)

<h2>Changes for Version 0.10.0 (2023-01-24)</h2>
  *  Remove support for endpoints <tt>/j, /m, /q, /p, /v</tt>. Their functions
     are merged into endpoint <tt>/z</tt>. This was announced in version 0.9.0.
     Please use only client library with at least version 0.10.0 too.
     (breaking: api)
  *  Remove support for runtime configuration key <tt>footer-html</tt>. Use
     <tt>footer-zettel</tt> instead. Deprecated in version 0.9.0.
     (breaking: webui)
  *  Save a query into a zettel to freeze it.
     (major: webui)
  *  Allow to show all used metadata keys, linked with their occurrences and
     their values.
     (minor: webui)
  *  Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding
     instead.
     (deprecated)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_9"></a>
<a name="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.
146
147
148
149
150
151
152
153

154
155
156
157
158
159
160
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83







-
+







     endpoint are still available and the new funnctionality is still there.
     This is a warning to prepare for some breaking changes in v0.10.0. This
     also affects the API client implementation.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_8"></a>
<a name="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.
186
187
188
189
190
191
192
193

194
195
196
197
198
199
200
109
110
111
112
113
114
115

116
117
118
119
120
121
122
123







-
+







     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 id="0_7"></a>
<a name="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>.
273
274
275
276
277
278
279
280

281
282
283
284
285
286
287
196
197
198
199
200
201
202

203
204
205
206
207
208
209
210







-
+







  *  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 id="0_6"></a>
<a name="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)
314
315
316
317
318
319
320
321

322
323
324
325
326
327
328
237
238
239
240
241
242
243

244
245
246
247
248
249
250
251







-
+







     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 id="0_5_0"></a>
<a name="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)
379
380
381
382
383
384
385
386

387
388
389
390
391
392
393
302
303
304
305
306
307
308

309
310
311
312
313
314
315
316







-
+







     (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 id="0_4"></a>
<a name="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.
435
436
437
438
439
440
441
442

443
444
445
446
447
448
449
358
359
360
361
362
363
364

365
366
367
368
369
370
371
372







-
+







     (minor: box)
  *  Disallow to cache the authentication cookie. Will remove most unexpected
     log-outs when using a mobile device.
     (minor: webui)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_3"></a>
<a name="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)
457
458
459
460
461
462
463
464

465
466
467
468
469
470
471
380
381
382
383
384
385
386

387
388
389
390
391
392
393
394







-
+







     (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 id="0_2"></a>
<a name="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)
523
524
525
526
527
528
529
530

531
532
533
534
535
536
537
446
447
448
449
450
451
452

453
454
455
456
457
458
459
460







-
+







     of just a boolean value that there were duplicate file names.
     (minor)
  *  Document autostarting Zettelstore on Windows, macOS, and Linux.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_1"></a><a id="0_1_0"></a>
<a name="0_1"></a><a name="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.
588
589
590
591
592
593
594
595

596
597
598
599
600
601
602
511
512
513
514
515
516
517

518
519
520
521
522
523
524
525







-
+







  *  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 id="0_0_15"></a>
<a name="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
643
644
645
646
647
648
649
650

651
652
653
654
655
656
657
566
567
568
569
570
571
572

573
574
575
576
577
578
579
580







-
+







     zettel in various encodings.
     (minor: api)
  *  Fix: do not list a shadowed zettel that matches the select criteria.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_0_14"></a>
<a name="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.
692
693
694
695
696
697
698
699

700
701
702
703
704
705
706
615
616
617
618
619
620
621

622
623
624
625
626
627
628
629







-
+







     (bug)
  *  The phase &ldquo;filtering a zettel list&rdquo; is more precise
     &ldquo;selecting zettel&rdquo;
     (documentation)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_0_13"></a>
<a name="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.
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
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







-
+


















-
+







  *  Add zettelmarkup syntax for a table row that should be ignored:
     <tt>|%</tt>. This allows to paste output of the administrator console into
     a zettel.
     (minor: zmk)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_0_12"></a>
<a name="0_0_12"></a>
<h2>Changes for Version 0.0.12 (2021-04-16)</h2>
  *  Raise the per-process limit of open files on macOS to 1.048.576. This
     allows most macOS users to use at least 500.000 zettel. That should be
     enough for the near future.
     (major)
  *  Mitigate the shortcomings of the macOS version by introducing types of
     directory boxes. The original directory box type is now called "notify"
     (the default value). There is a new type called "simple". This new type
     does not notify Zettelstore when some of the underlying Zettel files
     change.
     (major)
  *  Add new startup configuration <tt>default-dir-box-type</tt>, which gives
     the default value for specifying a directory box type. The default value
     is &ldquo;notify&rdquo;. On macOS, the default value may be changed
     &ldquo;simple&rdquo; if some errors occur while raising the per-process
     limit of open files.
     (minor)

<a id="0_0_11"></a>
<a name="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
796
797
798
799
800
801
802
803

804
805
806
807
808
809
810
719
720
721
722
723
724
725

726
727
728
729
730
731
732
733







-
+







A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
  #  You update the per-process limit of open files on macOS.
  #  You setup a virtualization environment to run Zettelstore on Linux or Windows.
  #  You wait for version 0.0.12 which addresses this issue.

<a id="0_0_10"></a>
<a name="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
840
841
842
843
844
845
846
847

848
849
850
851
852
853
854
763
764
765
766
767
768
769

770
771
772
773
774
775
776
777







-
+







     (&ldquo;&#8599;&#65038;&rdquo;), which also needed the additional
     &ldquo;&amp;#xfe0e;&rdquo; to disable the conversion to an emoji on iPadOS.
     (minor: webui)
  *  A pre-build binary for macOS ARM64 (also known as Apple silicon) is available.
     (minor: infrastructure)
  *  Many smaller bug fixes and improvements, to the software and to the documentation.

<a id="0_0_9"></a>
<a name="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.
920
921
922
923
924
925
926
927

928
929
930
931
932
933
934
843
844
845
846
847
848
849

850
851
852
853
854
855
856
857







-
+







             <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 id="0_0_8"></a>
<a name="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.
1008
1009
1010
1011
1012
1013
1014
1015

1016
1017
1018
1019
1020
1021
1022
1023

1024
1025
1026
1027
1028
1029
1030
931
932
933
934
935
936
937

938
939
940
941
942
943
944
945

946
947
948
949
950
951
952
953







-
+







-
+







  *  (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 id="0_0_7"></a>
<a name="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 id="0_0_6"></a>
<a name="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>
1070
1071
1072
1073
1074
1075
1076
1077

1078
1079
1080
1081
1082
1083
1084
993
994
995
996
997
998
999

1000
1001
1002
1003
1004
1005
1006
1007







-
+







             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 put after an
  *  (minor) Change the default value for some visual sugar putd 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
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
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







-
+






-
+





-
+





-
+










-
+


             <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 id="0_0_5"></a>
<a name="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 id="0_0_4"></a>
<a name="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 id="0_0_3"></a>
<a name="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 id="0_0_2"></a>
<a name="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 id="0_0_1"></a>
<a name="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.11.1</code> (2023-03-28).
Build: <code>v0.10.0</code> (2023-01-24).

  *  [/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.11.1-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.11.1-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/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.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-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.11.1.zip|here].
[/uv/manual-0.10.0.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.11.1 (2023-03-28)</h3>
<h3>Latest Release: 0.10.0 (2022-12-12)</h3>
  *  [./download.wiki|Download]
  *  [./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]
  *  [./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]
  *  [./plan.wiki|Limitations and planned improvements]
  *  [/timeline?t=release|Timeline of all past releases]

<hr>
<h2>Build instructions</h2>
Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read
Just install [https://golang.org/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;).